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:
yahaozhang
2025-07-20 14:45:43 +08:00
parent 479f671edc
commit 36c5d7f760
16 changed files with 618 additions and 72 deletions

View File

@@ -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
View 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. 当前实现为开发测试版本,注重功能实现,生产环境应加强安全性

View 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;
}
}

View 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() {
// 防止实例化
}
}

View File

@@ -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

View 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);
}
}

View File

@@ -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, "密码重置失败");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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")

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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: