diff --git a/backend/src/main/java/com/openisle/config/CachingConfig.java b/backend/src/main/java/com/openisle/config/CachingConfig.java index 9f13f65d4..565902db7 100644 --- a/backend/src/main/java/com/openisle/config/CachingConfig.java +++ b/backend/src/main/java/com/openisle/config/CachingConfig.java @@ -40,6 +40,8 @@ public class CachingConfig { public static final String CATEGORY_CACHE_NAME="openisle_categories"; // 在线人数缓存名 public static final String ONLINE_CACHE_NAME="openisle_online"; + // 注册验证码 + public static final String VERIFY_CACHE_NAME="openisle_verify"; /** * 自定义Redis的序列化器 diff --git a/backend/src/main/java/com/openisle/controller/AuthController.java b/backend/src/main/java/com/openisle/controller/AuthController.java index 49ad74d0e..0056c3c9b 100644 --- a/backend/src/main/java/com/openisle/controller/AuthController.java +++ b/backend/src/main/java/com/openisle/controller/AuthController.java @@ -1,18 +1,22 @@ package com.openisle.controller; +import com.openisle.config.CachingConfig; import com.openisle.dto.*; import com.openisle.exception.FieldException; import com.openisle.model.RegisterMode; import com.openisle.model.User; import com.openisle.repository.UserRepository; import com.openisle.service.*; +import com.openisle.util.VerifyType; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/api/auth") @@ -56,7 +60,8 @@ public class AuthController { User user = userService.registerWithInvite( req.getUsername(), req.getEmail(), req.getPassword()); inviteService.consume(req.getInviteToken(), user.getUsername()); - emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); + // 发送确认邮件 + userService.sendVerifyMail(user, VerifyType.REGISTER); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(user.getUsername()), "reason_code", "INVITE_APPROVED" @@ -70,7 +75,8 @@ public class AuthController { } User user = userService.register( req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode()); - emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); + // 发送确认邮件 + userService.sendVerifyMail(user, VerifyType.REGISTER); if (!user.isApproved()) { notificationService.createRegisterRequestNotifications(user, user.getRegisterReason()); } @@ -79,13 +85,12 @@ public class AuthController { @PostMapping("/verify") public ResponseEntity verify(@RequestBody VerifyRequest req) { - boolean ok = userService.verifyCode(req.getUsername(), req.getCode()); + Optional userOpt = userService.findByUsername(req.getUsername()); + if (userOpt.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials")); + } + boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.REGISTER); if (ok) { - Optional userOpt = userService.findByUsername(req.getUsername()); - if (userOpt.isEmpty()) { - return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials")); - } - User user = userOpt.get(); if (user.isApproved()) { @@ -122,7 +127,7 @@ public class AuthController { User user = userOpt.get(); if (!user.isVerified()) { user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode()); - emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); + userService.sendVerifyMail(user, VerifyType.REGISTER); return ResponseEntity.badRequest().body(Map.of( "error", "User not verified", "reason_code", "NOT_VERIFIED", @@ -417,14 +422,17 @@ public class AuthController { if (userOpt.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("error", "User not found")); } - String code = userService.generatePasswordResetCode(req.getEmail()); - emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code); + userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD); return ResponseEntity.ok(Map.of("message", "Verification code sent")); } @PostMapping("/forgot/verify") public ResponseEntity verifyReset(@RequestBody VerifyForgotRequest req) { - boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode()); + Optional userOpt = userService.findByEmail(req.getEmail()); + if (userOpt.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "User not found")); + } + boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.RESET_PASSWORD); if (ok) { String username = userService.findByEmail(req.getEmail()).get().getUsername(); return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username))); diff --git a/backend/src/main/java/com/openisle/service/UserService.java b/backend/src/main/java/com/openisle/service/UserService.java index 8c0dc4432..a3352901f 100644 --- a/backend/src/main/java/com/openisle/service/UserService.java +++ b/backend/src/main/java/com/openisle/service/UserService.java @@ -1,5 +1,6 @@ package com.openisle.service; +import com.openisle.config.CachingConfig; import com.openisle.model.User; import com.openisle.model.Role; import com.openisle.service.PasswordValidator; @@ -7,13 +8,18 @@ import com.openisle.service.UsernameValidator; import com.openisle.service.AvatarGenerator; import com.openisle.exception.FieldException; import com.openisle.repository.UserRepository; +import com.openisle.util.VerifyType; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Objects; import java.util.Optional; import java.util.Random; +import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @@ -25,6 +31,10 @@ public class UserService { private final ImageUploader imageUploader; private final AvatarGenerator avatarGenerator; + private final RedisTemplate redisTemplate; + + private final EmailSender emailService; + public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) { usernameValidator.validate(username); passwordValidator.validate(password); @@ -38,7 +48,7 @@ public class UserService { // 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码 u.setEmail(email); // 若不允许改邮箱可去掉 u.setPassword(passwordEncoder.encode(password)); - u.setVerificationCode(genCode()); +// u.setVerificationCode(genCode()); u.setRegisterReason(reason); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); return userRepository.save(u); @@ -54,7 +64,7 @@ public class UserService { // 未验证 → 允许“重注册” u.setUsername(username); // 若不允许改用户名可去掉 u.setPassword(passwordEncoder.encode(password)); - u.setVerificationCode(genCode()); +// u.setVerificationCode(genCode()); u.setRegisterReason(reason); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); return userRepository.save(u); @@ -67,7 +77,7 @@ public class UserService { user.setPassword(passwordEncoder.encode(password)); user.setRole(Role.USER); user.setVerified(false); - user.setVerificationCode(genCode()); +// user.setVerificationCode(genCode()); user.setAvatar(avatarGenerator.generate(username)); user.setRegisterReason(reason); user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); @@ -77,7 +87,7 @@ public class UserService { public User registerWithInvite(String username, String email, String password) { User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT); user.setVerified(true); - user.setVerificationCode(genCode()); +// user.setVerificationCode(genCode()); return userRepository.save(user); } @@ -85,16 +95,58 @@ public class UserService { return String.format("%06d", new Random().nextInt(1000000)); } - public boolean verifyCode(String username, String code) { - Optional userOpt = userRepository.findByUsername(username); - if (userOpt.isPresent() && code.equals(userOpt.get().getVerificationCode())) { - User user = userOpt.get(); - user.setVerified(true); - user.setVerificationCode(null); - userRepository.save(user); - return true; + /** + * 将验证码存入缓存,并发送邮件 + * @param user + */ + public void sendVerifyMail(User user, VerifyType verifyType){ + //缓存验证码 + String code = genCode(); + String key; + String subject; + String content = "您的验证码是:" + code; + // 注册类型 + if(verifyType.equals(VerifyType.REGISTER)){ + key = CachingConfig.VERIFY_CACHE_NAME + ":register:code:" + user.getUsername(); + subject = "在网站填写验证码以验证(有效期为5分钟)"; + }else { + // 重置密码 + key = CachingConfig.VERIFY_CACHE_NAME + ":reset_password:code:" + user.getUsername(); + subject = "请填写验证码以重置密码(有效期为5分钟)"; } - return false; + + redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);// 五分钟后验证码过期 + emailService.sendEmail(user.getEmail(), subject, content); + } + + /** + * 验证code是否正确 + * @param user + * @param code + * @param verifyType + * @return + */ + public boolean verifyCode(User user, String code, VerifyType verifyType) { + // 生成key + String key1 = VerifyType.REGISTER.equals(verifyType)?":register:code:":":reset_password:code:"; + String key = CachingConfig.VERIFY_CACHE_NAME + key1 + user.getUsername(); + // 这里不能使用getAndDelete,需要6.x版本 + String cachedCode = (String)redisTemplate.opsForValue().get(key); + // 如果校验code过期或者不存在 + // 或者校验code不一致 + if(Objects.isNull(cachedCode) + || !cachedCode.equals(code)){ + return false; + } + // 注册模式需要设置已经确认 + if(VerifyType.REGISTER.equals(verifyType)){ + user.setVerified(true); + userRepository.save(user); + } + // 走到这里说明验证成功删除验证码 + redisTemplate.delete(key); + return true; + } public Optional authenticate(String username, String password) { @@ -165,26 +217,6 @@ public class UserService { return userRepository.save(user); } - public String generatePasswordResetCode(String email) { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); - String code = genCode(); - user.setPasswordResetCode(code); - userRepository.save(user); - return code; - } - - public boolean verifyPasswordResetCode(String email, String code) { - Optional userOpt = userRepository.findByEmail(email); - if (userOpt.isPresent() && code.equals(userOpt.get().getPasswordResetCode())) { - User user = userOpt.get(); - user.setPasswordResetCode(null); - userRepository.save(user); - return true; - } - return false; - } - public User updatePassword(String username, String newPassword) { passwordValidator.validate(newPassword); User user = userRepository.findByUsername(username) diff --git a/backend/src/main/java/com/openisle/util/VerifyType.java b/backend/src/main/java/com/openisle/util/VerifyType.java new file mode 100644 index 000000000..885471a5c --- /dev/null +++ b/backend/src/main/java/com/openisle/util/VerifyType.java @@ -0,0 +1,20 @@ +package com.openisle.util; + +/** + * 验证码类型 + * @author smallclover + * @since 2025-09-08 + */ +public enum VerifyType { + REGISTER(1), + RESET_PASSWORD(2); + private final int code; + + VerifyType(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/backend/src/test/java/com/openisle/controller/AuthControllerTest.java b/backend/src/test/java/com/openisle/controller/AuthControllerTest.java index ff08af55d..283e1d34c 100644 --- a/backend/src/test/java/com/openisle/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/AuthControllerTest.java @@ -4,6 +4,7 @@ import com.openisle.model.User; import com.openisle.service.*; import com.openisle.model.RegisterMode; import com.openisle.repository.UserRepository; +import com.openisle.util.VerifyType; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -71,7 +72,9 @@ class AuthControllerTest { @Test void verifyCodeEndpoint() throws Exception { - Mockito.when(userService.verifyCode("u", "123")).thenReturn(true); + User user = new User(); + user.setUsername("u"); + Mockito.when(userService.verifyCode(user, "123", VerifyType.REGISTER)).thenReturn(true); Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token"); mockMvc.perform(post("/api/auth/verify")