From 36c5d7f760226cf51aaa6f25928b67a919132553 Mon Sep 17 00:00:00 2001 From: yahaozhang Date: Sun, 20 Jul 2025 14:45:43 +0800 Subject: [PATCH] fix UserType enum mapping and implement unified API response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 4 +- README.md | 54 ++++++ .../com/yundage/chat/common/ApiResponse.java | 99 ++++++++++ .../com/yundage/chat/common/ErrorCode.java | 38 ++++ .../yundage/chat/config/SecurityConfig.java | 26 ++- .../yundage/chat/config/UserTypeHandler.java | 38 ++++ .../chat/controller/AuthController.java | 119 +++++++++--- .../chat/controller/UserController.java | 37 +++- .../com/yundage/chat/dto/LoginRequest.java | 24 +-- .../com/yundage/chat/dto/RegisterRequest.java | 13 +- .../chat/dto/VerificationCodeRequest.java | 18 ++ .../yundage/chat/dto/VerifyCodeRequest.java | 29 +++ .../java/com/yundage/chat/entity/User.java | 2 +- .../java/com/yundage/chat/enums/UserType.java | 9 + .../com/yundage/chat/service/UserService.java | 176 +++++++++++++++--- src/main/resources/application.yml | 4 + 16 files changed, 618 insertions(+), 72 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/com/yundage/chat/common/ApiResponse.java create mode 100644 src/main/java/com/yundage/chat/common/ErrorCode.java create mode 100644 src/main/java/com/yundage/chat/config/UserTypeHandler.java create mode 100644 src/main/java/com/yundage/chat/dto/VerificationCodeRequest.java create mode 100644 src/main/java/com/yundage/chat/dto/VerifyCodeRequest.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8253cef..a7d4b1d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,9 @@ "Bash(mvn:*)", "Bash(./mvnw clean compile)", "Bash(cd:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(find:*)", + "Bash(git add:*)" ], "deny": [] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..4848508 --- /dev/null +++ b/README.md @@ -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. 当前实现为开发测试版本,注重功能实现,生产环境应加强安全性 \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/common/ApiResponse.java b/src/main/java/com/yundage/chat/common/ApiResponse.java new file mode 100644 index 0000000..64cdbaa --- /dev/null +++ b/src/main/java/com/yundage/chat/common/ApiResponse.java @@ -0,0 +1,99 @@ +package com.yundage.chat.common; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + 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 ApiResponse success() { + return new ApiResponse<>(ErrorCode.SUCCESS, "操作成功"); + } + + public static ApiResponse success(T data) { + return new ApiResponse<>(ErrorCode.SUCCESS, "操作成功", data); + } + + public static ApiResponse success(String message, T data) { + return new ApiResponse<>(ErrorCode.SUCCESS, message, data); + } + + // 失败响应 + public static ApiResponse error(int code, String message) { + return new ApiResponse<>(code, message); + } + + public static ApiResponse error(String message) { + return new ApiResponse<>(ErrorCode.INTERNAL_ERROR, message); + } + + // 常用错误响应 + public static ApiResponse badRequest(String message) { + return new ApiResponse<>(ErrorCode.BAD_REQUEST, message); + } + + public static ApiResponse unauthorized(String message) { + return new ApiResponse<>(ErrorCode.UNAUTHORIZED, message); + } + + public static ApiResponse forbidden(String message) { + return new ApiResponse<>(ErrorCode.FORBIDDEN, message); + } + + public static ApiResponse 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/common/ErrorCode.java b/src/main/java/com/yundage/chat/common/ErrorCode.java new file mode 100644 index 0000000..3d48440 --- /dev/null +++ b/src/main/java/com/yundage/chat/common/ErrorCode.java @@ -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() { + // 防止实例化 + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/config/SecurityConfig.java b/src/main/java/com/yundage/chat/config/SecurityConfig.java index 612c495..0222d0f 100644 --- a/src/main/java/com/yundage/chat/config/SecurityConfig.java +++ b/src/main/java/com/yundage/chat/config/SecurityConfig.java @@ -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 diff --git a/src/main/java/com/yundage/chat/config/UserTypeHandler.java b/src/main/java/com/yundage/chat/config/UserTypeHandler.java new file mode 100644 index 0000000..8695d24 --- /dev/null +++ b/src/main/java/com/yundage/chat/config/UserTypeHandler.java @@ -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 { + + @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); + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/controller/AuthController.java b/src/main/java/com/yundage/chat/controller/AuthController.java index 4003e88..4da0fb6 100644 --- a/src/main/java/com/yundage/chat/controller/AuthController.java +++ b/src/main/java/com/yundage/chat/controller/AuthController.java @@ -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 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 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 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 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 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 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 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 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 error = new HashMap<>(); - error.put("error", e.getMessage()); - return ResponseEntity.badRequest().body(error); + return ApiResponse.error(ErrorCode.INTERNAL_ERROR, "密码重置失败"); } } } \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/controller/UserController.java b/src/main/java/com/yundage/chat/controller/UserController.java index edd332a..bce748e 100644 --- a/src/main/java/com/yundage/chat/controller/UserController.java +++ b/src/main/java/com/yundage/chat/controller/UserController.java @@ -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 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); } } \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/LoginRequest.java b/src/main/java/com/yundage/chat/dto/LoginRequest.java index a11b1ea..3514b21 100644 --- a/src/main/java/com/yundage/chat/dto/LoginRequest.java +++ b/src/main/java/com/yundage/chat/dto/LoginRequest.java @@ -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; } } \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/RegisterRequest.java b/src/main/java/com/yundage/chat/dto/RegisterRequest.java index 6dfcb46..5aade8a 100644 --- a/src/main/java/com/yundage/chat/dto/RegisterRequest.java +++ b/src/main/java/com/yundage/chat/dto/RegisterRequest.java @@ -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; } } \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/VerificationCodeRequest.java b/src/main/java/com/yundage/chat/dto/VerificationCodeRequest.java new file mode 100644 index 0000000..6733386 --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/VerificationCodeRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/VerifyCodeRequest.java b/src/main/java/com/yundage/chat/dto/VerifyCodeRequest.java new file mode 100644 index 0000000..9a03eee --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/VerifyCodeRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/entity/User.java b/src/main/java/com/yundage/chat/entity/User.java index d42470f..3f77e3c 100644 --- a/src/main/java/com/yundage/chat/entity/User.java +++ b/src/main/java/com/yundage/chat/entity/User.java @@ -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") diff --git a/src/main/java/com/yundage/chat/enums/UserType.java b/src/main/java/com/yundage/chat/enums/UserType.java index 48cf007..106f83e 100644 --- a/src/main/java/com/yundage/chat/enums/UserType.java +++ b/src/main/java/com/yundage/chat/enums/UserType.java @@ -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); + } } \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/service/UserService.java b/src/main/java/com/yundage/chat/service/UserService.java index 87d92de..8c05143 100644 --- a/src/main/java/com/yundage/chat/service/UserService.java +++ b/src/main/java/com/yundage/chat/service/UserService.java @@ -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 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 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; + } + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5782731..59e53df 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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: