fix UserType enum mapping and implement unified API response
- Fix UserType enum mapping issue by adding fromValue method and custom type handler - Create unified API response structure with ErrorCode constants - Update AuthController to use standardized response format with proper error codes - Add verification code authentication system replacing password-based auth - Improve Swagger documentation with detailed API annotations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,9 @@
|
||||
"Bash(mvn:*)",
|
||||
"Bash(./mvnw clean compile)",
|
||||
"Bash(cd:*)",
|
||||
"Bash(ls:*)"
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
54
README.md
Normal file
54
README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 验证码认证实现说明
|
||||
|
||||
本项目实现了基于验证码的用户认证机制,不再使用密码进行登录和注册。
|
||||
|
||||
## 主要变更
|
||||
|
||||
1. **验证码请求和验证DTO**
|
||||
- `VerificationCodeRequest`: 请求发送验证码的DTO
|
||||
- `VerifyCodeRequest`: 验证验证码的DTO
|
||||
- 修改了`LoginRequest`和`RegisterRequest`,移除密码字段,改为使用验证码
|
||||
|
||||
2. **认证控制器**
|
||||
- 新增了`/api/auth/send-code`接口用于发送验证码
|
||||
- 修改了登录和注册接口以使用验证码认证
|
||||
|
||||
3. **用户服务**
|
||||
- 实现了验证码生成、存储和验证逻辑
|
||||
- 在开发模式下,验证码会在控制台输出
|
||||
- 支持使用万能验证码(默认为"123456")用于测试
|
||||
|
||||
4. **配置**
|
||||
- 在`application.yml`中添加了验证码相关的配置
|
||||
- 支持配置验证码过期时间、万能验证码和开发模式
|
||||
|
||||
5. **安全配置**
|
||||
- 更新了`SecurityConfig`以支持跨域请求
|
||||
- 优化了安全配置结构
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 用户注册/登录流程:
|
||||
- 用户输入邮箱或手机号,请求发送验证码
|
||||
- 系统生成验证码,在开发模式下控制台会输出验证码
|
||||
- 用户输入收到的验证码进行注册或登录
|
||||
|
||||
2. 万能验证码:
|
||||
- 在开发或测试环境中,可以使用配置的万能验证码(默认为"123456")
|
||||
- 无需等待验证码发送,直接使用万能验证码即可
|
||||
|
||||
3. 配置说明:
|
||||
```yaml
|
||||
app:
|
||||
verification-code:
|
||||
expiration: 300 # 验证码有效期(秒)
|
||||
master-code: "123456" # 万能验证码
|
||||
development-mode: true # 开发模式(控制台输出验证码)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 实际生产环境中,应使用Redis等缓存存储验证码,而非内存存储
|
||||
2. 需实现真实的短信和邮件发送服务来发送验证码
|
||||
3. 生产环境应禁用万能验证码和开发模式
|
||||
4. 当前实现为开发测试版本,注重功能实现,生产环境应加强安全性
|
||||
99
src/main/java/com/yundage/chat/common/ApiResponse.java
Normal file
99
src/main/java/com/yundage/chat/common/ApiResponse.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.yundage.chat.common;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
private Long timestamp;
|
||||
|
||||
public ApiResponse() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public ApiResponse(int code, String message) {
|
||||
this();
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ApiResponse(int code, String message, T data) {
|
||||
this(code, message);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
// 成功响应
|
||||
public static <T> ApiResponse<T> success() {
|
||||
return new ApiResponse<>(ErrorCode.SUCCESS, "操作成功");
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(ErrorCode.SUCCESS, "操作成功", data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return new ApiResponse<>(ErrorCode.SUCCESS, message, data);
|
||||
}
|
||||
|
||||
// 失败响应
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
return new ApiResponse<>(code, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(ErrorCode.INTERNAL_ERROR, message);
|
||||
}
|
||||
|
||||
// 常用错误响应
|
||||
public static <T> ApiResponse<T> badRequest(String message) {
|
||||
return new ApiResponse<>(ErrorCode.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> unauthorized(String message) {
|
||||
return new ApiResponse<>(ErrorCode.UNAUTHORIZED, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> forbidden(String message) {
|
||||
return new ApiResponse<>(ErrorCode.FORBIDDEN, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> notFound(String message) {
|
||||
return new ApiResponse<>(ErrorCode.NOT_FOUND, message);
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/yundage/chat/common/ErrorCode.java
Normal file
38
src/main/java/com/yundage/chat/common/ErrorCode.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.yundage.chat.common;
|
||||
|
||||
public class ErrorCode {
|
||||
|
||||
// 通用错误码
|
||||
public static final int SUCCESS = 200;
|
||||
public static final int BAD_REQUEST = 400;
|
||||
public static final int UNAUTHORIZED = 401;
|
||||
public static final int FORBIDDEN = 403;
|
||||
public static final int NOT_FOUND = 404;
|
||||
public static final int INTERNAL_ERROR = 500;
|
||||
|
||||
// 认证相关错误码 (1000-1999)
|
||||
public static final int VERIFICATION_CODE_INVALID = 1001;
|
||||
public static final int VERIFICATION_CODE_EXPIRED = 1002;
|
||||
public static final int VERIFICATION_CODE_SEND_FAILED = 1003;
|
||||
public static final int USER_NOT_FOUND = 1004;
|
||||
public static final int USER_ALREADY_EXISTS = 1005;
|
||||
public static final int EMAIL_ALREADY_REGISTERED = 1006;
|
||||
public static final int PHONE_ALREADY_REGISTERED = 1007;
|
||||
public static final int PASSWORD_RESET_TOKEN_INVALID = 1008;
|
||||
public static final int LOGIN_FAILED = 1009;
|
||||
public static final int REGISTER_FAILED = 1010;
|
||||
|
||||
// 业务相关错误码 (2000-2999)
|
||||
public static final int CHAT_NOT_FOUND = 2001;
|
||||
public static final int MESSAGE_SEND_FAILED = 2002;
|
||||
public static final int FILE_UPLOAD_FAILED = 2003;
|
||||
|
||||
// 权限相关错误码 (3000-3999)
|
||||
public static final int ACCESS_DENIED = 3001;
|
||||
public static final int TOKEN_EXPIRED = 3002;
|
||||
public static final int TOKEN_INVALID = 3003;
|
||||
|
||||
private ErrorCode() {
|
||||
// 防止实例化
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -47,19 +52,38 @@ public class SecurityConfig {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(Arrays.asList("*"));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
|
||||
configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf(AbstractHttpConfigurer::disable)
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// 认证相关接口
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
// 公开API
|
||||
.requestMatchers("/api/public/**").permitAll()
|
||||
// 健康检查
|
||||
.requestMatchers("/health").permitAll()
|
||||
// Swagger API文档
|
||||
.requestMatchers("/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/swagger-ui.html").permitAll()
|
||||
.requestMatchers("/api-docs/**").permitAll()
|
||||
.requestMatchers("/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/swagger-resources/**").permitAll()
|
||||
.requestMatchers("/webjars/**").permitAll()
|
||||
// 其他所有请求需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.sessionManagement(session -> session
|
||||
|
||||
38
src/main/java/com/yundage/chat/config/UserTypeHandler.java
Normal file
38
src/main/java/com/yundage/chat/config/UserTypeHandler.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.yundage.chat.config;
|
||||
|
||||
import com.yundage.chat.enums.UserType;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@MappedTypes(UserType.class)
|
||||
public class UserTypeHandler extends BaseTypeHandler<UserType> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, UserType parameter, JdbcType jdbcType) throws SQLException {
|
||||
ps.setString(i, parameter.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserType getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String value = rs.getString(columnName);
|
||||
return value == null ? null : UserType.fromValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String value = rs.getString(columnIndex);
|
||||
return value == null ? null : UserType.fromValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String value = cs.getString(columnIndex);
|
||||
return value == null ? null : UserType.fromValue(value);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.yundage.chat.controller;
|
||||
|
||||
import com.yundage.chat.common.ApiResponse;
|
||||
import com.yundage.chat.common.ErrorCode;
|
||||
import com.yundage.chat.dto.*;
|
||||
import com.yundage.chat.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -12,60 +16,125 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@Tag(name = "认证管理", description = "用户认证相关接口")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping("/send-code")
|
||||
@Operation(summary = "发送验证码", description = "向手机或邮箱发送验证码")
|
||||
@ApiResponses(value = {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "验证码发送成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "验证码发送失败")
|
||||
})
|
||||
public ApiResponse<?> sendVerificationCode(@Valid @RequestBody VerificationCodeRequest request) {
|
||||
try {
|
||||
String code = userService.sendVerificationCode(request.getContact());
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("message", "验证码已发送");
|
||||
// 仅在开发模式下返回验证码
|
||||
if (code != null) {
|
||||
data.put("code", code);
|
||||
}
|
||||
return ApiResponse.success("验证码发送成功", data);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error(ErrorCode.VERIFICATION_CODE_SEND_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
|
||||
@Operation(summary = "用户注册", description = "注册新用户账号")
|
||||
@ApiResponses(value = {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "注册成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "注册失败")
|
||||
})
|
||||
public ApiResponse<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
try {
|
||||
AuthResponse response = userService.register(request);
|
||||
return ResponseEntity.ok(response);
|
||||
return ApiResponse.success("注册成功", response);
|
||||
} catch (RuntimeException e) {
|
||||
String message = e.getMessage();
|
||||
if (message.contains("邮箱已被注册")) {
|
||||
return ApiResponse.error(ErrorCode.EMAIL_ALREADY_REGISTERED, message);
|
||||
} else if (message.contains("手机号已被注册")) {
|
||||
return ApiResponse.error(ErrorCode.PHONE_ALREADY_REGISTERED, message);
|
||||
} else if (message.contains("验证码")) {
|
||||
return ApiResponse.error(ErrorCode.VERIFICATION_CODE_INVALID, message);
|
||||
} else {
|
||||
return ApiResponse.error(ErrorCode.REGISTER_FAILED, message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
return ApiResponse.error(ErrorCode.REGISTER_FAILED, "注册失败");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
|
||||
@Operation(summary = "用户登录", description = "使用验证码登录")
|
||||
@ApiResponses(value = {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "登录成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "登录失败")
|
||||
})
|
||||
public ApiResponse<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
try {
|
||||
AuthResponse response = userService.login(request);
|
||||
return ResponseEntity.ok(response);
|
||||
return ApiResponse.success("登录成功", response);
|
||||
} catch (RuntimeException e) {
|
||||
String message = e.getMessage();
|
||||
if (message.contains("验证码无效") || message.contains("验证码已过期")) {
|
||||
return ApiResponse.error(ErrorCode.VERIFICATION_CODE_INVALID, message);
|
||||
} else if (message.contains("用户不存在")) {
|
||||
return ApiResponse.error(ErrorCode.USER_NOT_FOUND, message);
|
||||
} else {
|
||||
return ApiResponse.error(ErrorCode.LOGIN_FAILED, message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
return ApiResponse.error(ErrorCode.LOGIN_FAILED, "登录失败");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/forgot-password")
|
||||
public ResponseEntity<?> forgotPassword(@Valid @RequestBody PasswordResetRequest request) {
|
||||
@Operation(summary = "忘记密码", description = "发送密码重置邮件")
|
||||
@ApiResponses(value = {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "邮件发送成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "邮件发送失败")
|
||||
})
|
||||
public ApiResponse<?> forgotPassword(@Valid @RequestBody PasswordResetRequest request) {
|
||||
try {
|
||||
userService.requestPasswordReset(request);
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("message", "密码重置邮件已发送");
|
||||
return ResponseEntity.ok(response);
|
||||
return ApiResponse.success("密码重置邮件已发送");
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getMessage().contains("用户不存在")) {
|
||||
return ApiResponse.error(ErrorCode.USER_NOT_FOUND, e.getMessage());
|
||||
} else {
|
||||
return ApiResponse.error(ErrorCode.INTERNAL_ERROR, e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
return ApiResponse.error(ErrorCode.INTERNAL_ERROR, "邮件发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/reset-password")
|
||||
public ResponseEntity<?> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
|
||||
@Operation(summary = "重置密码", description = "使用token重置密码")
|
||||
@ApiResponses(value = {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "密码重置成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "密码重置失败")
|
||||
})
|
||||
public ApiResponse<?> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
|
||||
try {
|
||||
userService.resetPassword(request);
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("message", "密码重置成功");
|
||||
return ResponseEntity.ok(response);
|
||||
return ApiResponse.success("密码重置成功");
|
||||
} catch (RuntimeException e) {
|
||||
String message = e.getMessage();
|
||||
if (message.contains("重置令牌无效") || message.contains("已过期")) {
|
||||
return ApiResponse.error(ErrorCode.PASSWORD_RESET_TOKEN_INVALID, message);
|
||||
} else if (message.contains("用户不存在")) {
|
||||
return ApiResponse.error(ErrorCode.USER_NOT_FOUND, message);
|
||||
} else {
|
||||
return ApiResponse.error(ErrorCode.INTERNAL_ERROR, message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
return ApiResponse.error(ErrorCode.INTERNAL_ERROR, "密码重置失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@ package com.yundage.chat.controller;
|
||||
|
||||
import com.yundage.chat.entity.User;
|
||||
import com.yundage.chat.mapper.UserMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -10,22 +16,36 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@Tag(name = "用户管理", description = "用户CRUD操作接口")
|
||||
@SecurityRequirement(name = "Bearer Authentication")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
|
||||
@ApiResponse(responseCode = "200", description = "成功获取用户列表")
|
||||
public List<User> getAllUsers() {
|
||||
return userMapper.selectAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public User getUserById(@PathVariable Long id) {
|
||||
@Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "成功获取用户信息"),
|
||||
@ApiResponse(responseCode = "404", description = "用户不存在")
|
||||
})
|
||||
public User getUserById(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
return userMapper.selectOneById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建用户", description = "创建新的用户")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "用户创建成功"),
|
||||
@ApiResponse(responseCode = "400", description = "请求参数错误")
|
||||
})
|
||||
public User createUser(@RequestBody User user) {
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -34,7 +54,13 @@ public class UserController {
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public User updateUser(@PathVariable Long id, @RequestBody User user) {
|
||||
@Operation(summary = "更新用户", description = "更新指定用户的信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "用户更新成功"),
|
||||
@ApiResponse(responseCode = "404", description = "用户不存在"),
|
||||
@ApiResponse(responseCode = "400", description = "请求参数错误")
|
||||
})
|
||||
public User updateUser(@Parameter(description = "用户ID") @PathVariable Long id, @RequestBody User user) {
|
||||
user.setId(id);
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
userMapper.update(user);
|
||||
@@ -42,7 +68,12 @@ public class UserController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void deleteUser(@PathVariable Long id) {
|
||||
@Operation(summary = "删除用户", description = "根据ID删除用户")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "用户删除成功"),
|
||||
@ApiResponse(responseCode = "404", description = "用户不存在")
|
||||
})
|
||||
public void deleteUser(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
userMapper.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,26 @@ import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class LoginRequest {
|
||||
|
||||
@NotBlank(message = "用户名/邮箱不能为空")
|
||||
private String username;
|
||||
@NotBlank(message = "手机号或邮箱不能为空")
|
||||
private String contact;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
|
||||
// Getters and Setters
|
||||
public String getUsername() {
|
||||
return username;
|
||||
public String getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
public void setContact(String contact) {
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,8 @@ public class RegisterRequest {
|
||||
|
||||
private String phone;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")
|
||||
private String password;
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
|
||||
// Getters and Setters
|
||||
public String getUsername() {
|
||||
@@ -44,11 +43,11 @@ public class RegisterRequest {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yundage.chat.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class VerificationCodeRequest {
|
||||
|
||||
@NotBlank(message = "手机号或邮箱不能为空")
|
||||
private String contact;
|
||||
|
||||
// Getters and Setters
|
||||
public String getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public void setContact(String contact) {
|
||||
this.contact = contact;
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/yundage/chat/dto/VerifyCodeRequest.java
Normal file
29
src/main/java/com/yundage/chat/dto/VerifyCodeRequest.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.yundage.chat.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class VerifyCodeRequest {
|
||||
|
||||
@NotBlank(message = "手机号或邮箱不能为空")
|
||||
private String contact;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String code;
|
||||
|
||||
// Getters and Setters
|
||||
public String getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public void setContact(String contact) {
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class User implements UserDetails {
|
||||
@Column("avatar_url")
|
||||
private String avatarUrl;
|
||||
|
||||
@Column("user_type")
|
||||
@Column(value = "user_type", typeHandler = com.yundage.chat.config.UserTypeHandler.class)
|
||||
private UserType userType;
|
||||
|
||||
@Column("membership_level_id")
|
||||
|
||||
@@ -14,4 +14,13 @@ public enum UserType {
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static UserType fromValue(String value) {
|
||||
for (UserType type : UserType.values()) {
|
||||
if (type.value.equals(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown UserType value: " + value);
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,20 @@ import com.yundage.chat.mapper.PasswordResetTokenMapper;
|
||||
import com.yundage.chat.util.JwtUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
@@ -40,8 +44,89 @@ public class UserService {
|
||||
@Autowired
|
||||
private EmailService emailService;
|
||||
|
||||
// 从配置文件中获取验证码配置
|
||||
@Value("${app.verification-code.master-code:123456}")
|
||||
private String masterCode;
|
||||
|
||||
@Value("${app.verification-code.expiration:300}")
|
||||
private int codeExpirationSeconds;
|
||||
|
||||
@Value("${app.verification-code.development-mode:false}")
|
||||
private boolean developmentMode;
|
||||
|
||||
// 验证码存储,实际项目中应该使用Redis
|
||||
private final Map<String, VerificationCodeInfo> verificationCodes = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
* @param contact 联系方式(邮箱或手机号)
|
||||
* @return 验证码(仅开发模式返回)
|
||||
*/
|
||||
public String sendVerificationCode(String contact) {
|
||||
// 生成6位随机验证码
|
||||
String code = generateRandomCode();
|
||||
|
||||
// 存储验证码
|
||||
verificationCodes.put(contact, new VerificationCodeInfo(code, LocalDateTime.now()));
|
||||
|
||||
// TODO: 实际项目中,这里应该通过短信或邮件发送验证码
|
||||
// 如果是邮箱,可以使用邮件服务发送
|
||||
if (contact.contains("@")) {
|
||||
// emailService.sendVerificationCode(contact, code);
|
||||
} else {
|
||||
// 使用短信服务发送
|
||||
// smsService.sendVerificationCode(contact, code);
|
||||
}
|
||||
|
||||
// 在开发模式下,在控制台打印验证码
|
||||
if (developmentMode) {
|
||||
System.out.println("向 " + contact + " 发送验证码: " + code);
|
||||
}
|
||||
|
||||
return developmentMode ? code : null; // 仅在开发模式下返回验证码
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
* @param contact 联系方式
|
||||
* @param code 验证码
|
||||
* @return 是否验证成功
|
||||
*/
|
||||
private boolean verifyCode(String contact, String code) {
|
||||
// 检查万能验证码
|
||||
if (masterCode.equals(code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查验证码是否存在
|
||||
VerificationCodeInfo storedInfo = verificationCodes.get(contact);
|
||||
if (storedInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查验证码是否过期
|
||||
LocalDateTime expirationTime = storedInfo.timestamp.plusSeconds(codeExpirationSeconds);
|
||||
if (LocalDateTime.now().isAfter(expirationTime)) {
|
||||
verificationCodes.remove(contact); // 移除过期验证码
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查验证码是否匹配
|
||||
boolean isValid = storedInfo.code.equals(code);
|
||||
if (isValid) {
|
||||
verificationCodes.remove(contact); // 验证成功后移除验证码,防止重复使用
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AuthResponse register(RegisterRequest request) {
|
||||
// 验证验证码
|
||||
if (!verifyCode(request.getEmail() != null ? request.getEmail() : request.getPhone(), request.getCode())) {
|
||||
throw new RuntimeException("验证码无效或已过期");
|
||||
}
|
||||
|
||||
// 检查用户是否已存在
|
||||
if (request.getEmail() != null && existsByEmail(request.getEmail())) {
|
||||
throw new RuntimeException("邮箱已被注册");
|
||||
@@ -56,40 +141,54 @@ public class UserService {
|
||||
user.setUsername(request.getUsername());
|
||||
user.setEmail(request.getEmail());
|
||||
user.setPhone(request.getPhone());
|
||||
user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
|
||||
// 设置一个随机密码,因为不再使用密码登录
|
||||
user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString()));
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
userMapper.insert(user);
|
||||
|
||||
// 生成JWT令牌
|
||||
String token = jwtUtil.generateToken(user.getUsername(), user.getId());
|
||||
String token = jwtUtil.generateToken(user.getDisplayName(), user.getId());
|
||||
|
||||
return new AuthResponse(token, user.getId(), user.getDisplayName(),
|
||||
user.getEmail(), user.getPhone());
|
||||
}
|
||||
|
||||
public AuthResponse login(LoginRequest request) {
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||
);
|
||||
|
||||
User user = (User) authentication.getPrincipal();
|
||||
|
||||
// 更新最后登录时间
|
||||
user.setLastLoginAt(LocalDateTime.now());
|
||||
userMapper.update(user);
|
||||
|
||||
// 生成JWT令牌
|
||||
String token = jwtUtil.generateToken(user.getUsername(), user.getId());
|
||||
|
||||
return new AuthResponse(token, user.getId(), user.getDisplayName(),
|
||||
user.getEmail(), user.getPhone());
|
||||
|
||||
} catch (AuthenticationException e) {
|
||||
throw new RuntimeException("用户名或密码错误");
|
||||
// 验证验证码
|
||||
if (!verifyCode(request.getContact(), request.getCode())) {
|
||||
throw new RuntimeException("验证码无效或已过期");
|
||||
}
|
||||
|
||||
// 根据联系方式查找用户
|
||||
User user = findByContact(request.getContact());
|
||||
if (user == null) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
// 手动设置认证信息
|
||||
Authentication authentication = createAuthenticationToken(user);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
// 更新最后登录时间
|
||||
user.setLastLoginAt(LocalDateTime.now());
|
||||
userMapper.update(user);
|
||||
|
||||
// 生成JWT令牌
|
||||
String token = jwtUtil.generateToken(user.getDisplayName(), user.getId());
|
||||
|
||||
return new AuthResponse(token, user.getId(), user.getDisplayName(),
|
||||
user.getEmail(), user.getPhone());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建认证令牌
|
||||
*/
|
||||
private Authentication createAuthenticationToken(User user) {
|
||||
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
return new UsernamePasswordAuthenticationToken(user, null, authorities);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -138,6 +237,17 @@ public class UserService {
|
||||
tokenMapper.update(resetToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据联系方式(邮箱或手机号)查找用户
|
||||
*/
|
||||
public User findByContact(String contact) {
|
||||
if (contact.contains("@")) {
|
||||
return findByEmail(contact);
|
||||
} else {
|
||||
return findByPhone(contact);
|
||||
}
|
||||
}
|
||||
|
||||
public User findByEmail(String email) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.where(User::getEmail).eq(email);
|
||||
@@ -157,4 +267,26 @@ public class UserService {
|
||||
public boolean existsByPhone(String phone) {
|
||||
return findByPhone(phone) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机验证码
|
||||
*/
|
||||
private String generateRandomCode() {
|
||||
Random random = new Random();
|
||||
int code = 100000 + random.nextInt(900000); // 生成6位随机数
|
||||
return String.valueOf(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码信息类
|
||||
*/
|
||||
private static class VerificationCodeInfo {
|
||||
private final String code;
|
||||
private final LocalDateTime timestamp;
|
||||
|
||||
public VerificationCodeInfo(String code, LocalDateTime timestamp) {
|
||||
this.code = code;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@ jwt:
|
||||
# App configuration
|
||||
app:
|
||||
reset-password-url: http://localhost:3000/reset-password
|
||||
verification-code:
|
||||
expiration: 300 # 5 minutes in seconds
|
||||
master-code: "123456" # Master code for testing
|
||||
development-mode: true # Enable printing code to console in development mode
|
||||
|
||||
# Server configuration
|
||||
server:
|
||||
|
||||
Reference in New Issue
Block a user