1.添加邮箱验证

This commit is contained in:
2025-08-06 20:45:49 +08:00
parent fda3e45dc6
commit 2cfadf96dd
13 changed files with 382 additions and 20 deletions

Binary file not shown.

View File

@@ -0,0 +1,16 @@
package vvpkassistant.Tools;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
/*
* @author: ziin
* @date: 2025/8/6 20:15
*/
public class CacheHolder {
public static final Cache<String, String> VERIFICATION_MAIL = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}

View File

@@ -1,6 +1,7 @@
package vvpkassistant.User.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.extra.mail.Mail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vvpkassistant.CoinRecords.CoinRecords;
@@ -397,8 +398,8 @@ public class UserController {
}
@PostMapping("/resendMail")
public ResponseData<Object> resendMail(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(mailService.resendMail(userModelDTO));
public ResponseData<Object> resendMail(@RequestBody MailModel mailModel){
return ResponseData.success(mailService.resendMail(mailModel));
}
@GetMapping("/verificationMail")
@@ -406,6 +407,10 @@ public class UserController {
return ResponseData.success(userService.verificationMail(token));
}
@PostMapping("/forgetMail")
public ResponseData<Object> sendForgetPassWordMail(@RequestBody MailModel mailModel){
return ResponseData.success(userService.sendForgetPassWordMail(mailModel));
}
@GetMapping("/qrcode")
public ResponseData<Object> generatedQrcode(){
@@ -428,4 +433,30 @@ public class UserController {
userService.confirm(scanInfoDTO);
return ResponseData.success("");
}
@PostMapping("/logout")
public ResponseData<Object> logOut(@RequestBody UserModelDTO userModelDTO){
userService.logOut(userModelDTO.getId());
return ResponseData.success("");
}
@PostMapping("/setPassword")
public ResponseData<Object>setPassWord(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.setPassWord(userModelDTO));
}
@GetMapping("/resetPassword/")
public ResponseData<Object>resetPassWord(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.resetPassWord(userModelDTO));
}
@PostMapping("/updateUserMail")
public ResponseData<Object>updateUserMail(@RequestBody MailModel mailModel){
return ResponseData.success(userService.updateUserMail(mailModel));
}
@PostMapping("/sendUpdateMailConfirmMail")
public ResponseData<Object>sendUpdateMailConfirmMail(@RequestBody MailModel mailModel){
return ResponseData.success(mailService.sendUpdateConfirmMail(mailModel));
}
}

View File

@@ -21,6 +21,8 @@ public class UserModelDTO {
private Integer inviterId; // 邀请人id
private String email;
private String newPassword;
private String confirmPassword;
private String oldPassword;
private String password;
private String token;
}

View File

@@ -5,7 +5,7 @@ import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.mail.model.MailModel;
/*
@@ -30,4 +30,14 @@ public interface UserService extends IService<UserModel> {
void scanQrcode(ScanInfoDTO scanInfoDTO);
void confirm(ScanInfoDTO scanInfoDTO);
void logOut(Integer id);
boolean setPassWord(UserModelDTO userModelDTO);
Object sendForgetPassWordMail(MailModel mailModel);
Object resetPassWord(UserModelDTO userModelDTO);
Boolean updateUserMail(MailModel mailModel);
}

View File

@@ -12,9 +12,11 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.zxing.WriterException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.Tools.BcryptUtils;
import vvpkassistant.Tools.CacheHolder;
import vvpkassistant.Tools.QRCodeUtil;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
@@ -25,6 +27,7 @@ import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.enumeration.LoginStatusEnum;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import vvpkassistant.mail.service.MailService;
import javax.annotation.Resource;
@@ -53,6 +56,9 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
@Override
public UserModelVO loginWithMail(UserModelDTO model) {
@@ -99,7 +105,7 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
// 用户有密码的情况下重新设置密码
if (userInfo.getPassword() != null && userModelDTO.getOldPassword() != null) {
if (BcryptUtils.matchPassword(userModelDTO.getOldPassword(), userInfo.getPassword())) {
if (BcryptUtils.matchPassword(userInfo.getPassword(),userModelDTO.getOldPassword())) {
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR,"旧密码不正确");
@@ -150,6 +156,7 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
userDao.updateById(oldUser);
}
UserModelVO userModelVO = BeanUtil.copyProperties(userModelEntity, UserModelVO.class);
userModelVO.setHavaPassword(true);
userModelVO.setNewAccount(true);
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
@@ -251,4 +258,77 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
}
log.info("-------确认登录成功uuid:{}-------", scanInfoDTO.getUuid());
}
@Override
public void logOut(Integer id) {
StpUtil.logout(id);
}
@Override
public boolean setPassWord(UserModelDTO userModelDTO) {
UserModel userModel = userDao.selectById(userModelDTO.getId());
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModel.getPassword()!= null){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"已设置过密码");
}
if (userModelDTO.getPassword().length()< 6 ){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
if (!Objects.equals(userModelDTO.getPassword(), userModelDTO.getConfirmPassword())) {
log.error("密码{},确认密码{}",userModelDTO.getPassword(),userModelDTO.getConfirmPassword());
throw new BusinessException(ErrorCode.PARAMS_ERROR,"两次密码输入不一致");
}else{
UserModel saveEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
saveEntity.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
return userDao.updateById(saveEntity) == 1 ;
}
}
@Override
public Object sendForgetPassWordMail(MailModel mailModel) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getEmail, mailModel.getMailAddress())
.eq(UserModel::getStatus, 0)
.eq(UserModel::getMailVerification, 0));
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
mailService.sendForgetPassWordMail(mailModel.getMailAddress(),userModel.getId());
return true;
}
@Override
public Object resetPassWord(UserModelDTO userModelDTO) {
Integer i = SaTempUtil.parseToken(userModelDTO.getToken(), Integer.class);
UserModel userModel = userDao.selectById(i);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModelDTO.getPassword().equals(userModelDTO.getConfirmPassword())) {
UserModel updateEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
updateEntity.setPassword(BcryptUtils.encryptPassword(updateEntity.getPassword()));
return userDao.updateById(updateEntity) == 1;
}
return false;
}
@Override
public Boolean updateUserMail(MailModel mailModel) {
String mail = CacheHolder.VERIFICATION_MAIL.getIfPresent(mailModel.getCode());
if (mail != null && mail.isEmpty()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"验证码过期或验证码错误");
}
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getEmail, mail)
.eq(UserModel::getStatus, 0));
userModel.setEmail(mailModel.getMailAddress());
mailService.sendVerificationMail(mailModel.getMailAddress(),userModel.getId());
userModel.setMailVerification(1);
return userDao.updateById(userModel) == 1;
}
}

View File

@@ -49,7 +49,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/user/verificationMail",
"/user/activate",
"/user/qrcode",
"/user/check/*",
"/user/check/**",
"/user/scan",
"/user/confirm"

View File

@@ -9,5 +9,7 @@ import lombok.Data;
@Data
public class MailModel {
private String mailAddress;
private Integer code;
private Integer userId;
private Integer type;
private String code;
}

View File

@@ -1,6 +1,5 @@
package vvpkassistant.mail.service;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.mail.model.MailModel;
/*
@@ -13,7 +12,9 @@ public interface MailService {
void sendVerificationMail(String emailAddress,Integer userId);
Boolean resendMail(MailModel mailModel);
void sendForgetPassWordMail(String mailAddress, Integer id);
Boolean resendMail(UserModelDTO userModelDTO);
Object sendUpdateConfirmMail(MailModel mailModel);
}

View File

@@ -1,17 +1,23 @@
package vvpkassistant.mail.service;
import cn.dev33.satoken.temp.SaTempUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.mail.MailUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.Tools.CacheHolder;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/*
@@ -29,8 +35,15 @@ public class MailServiceImpl implements MailService {
@Value("${verificationMailUrl}")
private String verificationMailUrl;
@Value("${forgetPassWordUrl}")
private String forgetPassWordUrl;
private final Cache<String, Object> emailSendCache = Caffeine.newBuilder()
@Resource
private UserDao userDao;
private static final Cache<String, Object> emailSendCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@@ -365,12 +378,217 @@ public class MailServiceImpl implements MailService {
}
@Override
public Boolean resendMail(UserModelDTO userModelDTO) {
try {
sendMail(userModelDTO.getEmail(), userModelDTO.getId());
public Boolean resendMail(MailModel mailModel) {
Object ifPresent = emailSendCache.getIfPresent(mailModel.getMailAddress());
if (ifPresent == null) {
switch (mailModel.getType()) {
case 1:
sendMail(mailModel.getMailAddress(), mailModel.getUserId());
break;
case 2:
sendVerificationMail(mailModel.getMailAddress(), mailModel.getUserId());
break;
case 3:
sendForgetPassWordMail(mailModel.getMailAddress(), mailModel.getUserId());
break;
}
return true;
}catch (Exception e){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,e.getMessage());
}else {
throw new BusinessException(ErrorCode.EMAIL_SEND_FREQUENT);
}
}
@Override
public void sendForgetPassWordMail(String mailAddress, Integer userId) {
if (checkCache(mailAddress)){
String token = SaTempUtil.createToken(userId, 600);
MailUtil.send(mailAddress, "验证你的邮箱", "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>Account Activation</title>\n" +
" <style>\n" +
" * {\n" +
" margin: 0;\n" +
" padding: 0;\n" +
" box-sizing: border-box;\n" +
" }\n" +
"\n" +
" body {\n" +
" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n" +
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
" min-height: 100vh;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" padding: 20px;\n" +
" }\n" +
"\n" +
" .container {\n" +
" background: white;\n" +
" border-radius: 12px;\n" +
" box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n" +
" padding: 40px;\n" +
" text-align: center;\n" +
" max-width: 400px;\n" +
" width: 100%;\n" +
" }\n" +
"\n" +
" .icon {\n" +
" width: 60px;\n" +
" height: 60px;\n" +
" background: #4CAF50;\n" +
" border-radius: 50%;\n" +
" display: flex;\n" +
" align-items: center;\n" +
" justify-content: center;\n" +
" margin: 0 auto 20px;\n" +
" color: white;\n" +
" font-size: 24px;\n" +
" }\n" +
"\n" +
" h1 {\n" +
" color: #333;\n" +
" font-size: 24px;\n" +
" margin-bottom: 10px;\n" +
" font-weight: 600;\n" +
" }\n" +
"\n" +
" .subtitle {\n" +
" color: #666;\n" +
" font-size: 16px;\n" +
" margin-bottom: 30px;\n" +
" line-height: 1.5;\n" +
" }\n" +
"\n" +
" .activate-button {\n" +
" display: inline-block;\n" +
" background: #4CAF50;\n" +
" border: none;\n" +
" border-radius: 8px;\n" +
" padding: 15px 30px;\n" +
" margin: 20px 0;\n" +
" font-size: 18px;\n" +
" font-weight: bold;\n" +
" color: white;\n" +
" text-decoration: none;\n" +
" cursor: pointer;\n" +
" transition: all 0.3s ease;\n" +
" box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
" \n" +
" .activate-button:hover {\n" +
" background: #45a049;\n" +
" transform: translateY(-2px);\n" +
" box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);\n" +
" }\n" +
" \n" +
" .activate-button:active {\n" +
" transform: translateY(0);\n" +
" box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n" +
" }\n" +
"\n" +
" .info {\n" +
" background: #e3f2fd;\n" +
" border-left: 4px solid #2196F3;\n" +
" padding: 15px;\n" +
" margin: 20px 0;\n" +
" text-align: left;\n" +
" border-radius: 4px;\n" +
" }\n" +
"\n" +
" .info-title {\n" +
" font-weight: 600;\n" +
" color: #1976D2;\n" +
" margin-bottom: 5px;\n" +
" }\n" +
"\n" +
" .info-text {\n" +
" color: #424242;\n" +
" font-size: 14px;\n" +
" line-height: 1.4;\n" +
" }\n" +
"\n" +
" .footer {\n" +
" margin-top: 30px;\n" +
" padding-top: 20px;\n" +
" border-top: 1px solid #eee;\n" +
" color: #999;\n" +
" font-size: 12px;\n" +
" }\n" +
"\n" +
" @media (max-width: 480px) {\n" +
" .container {\n" +
" padding: 30px 20px;\n" +
" }\n" +
" \n" +
" .verification-code {\n" +
" font-size: 24px;\n" +
" letter-spacing: 2px;\n" +
" }\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <div class=\"icon\">✉</div>\n" +
" \n" +
" <h1>Verification Mail</h1>\n" +
" <p class=\"subtitle\">Please click the button below to reset your password</p>\n" +
" \n" +
" <a href=\""+ forgetPassWordUrl + token + "\" class=\"activate-button\">\n" +
" Reset PassWord\n" +
" </a>\n" +
" \n" +
" <div class=\"info\">\n" +
" <div class=\"info-title\">Important Notice:</div>\n" +
" <div class=\"info-text\">\n" +
" • This activation link is valid for 10 minutes<br>\n" +
" • Please do not share this link with anyone<br>\n" +
" • If you didn't register an account, please ignore this message\n" +
" </div>\n" +
" </div>\n" +
" \n" +
" <div class=\"footer\">\n" +
" This is an automated message. Please do not reply to this email.\n" +
" </div>\n" +
" </div>\n" +
"</body>\n" +
"</html>\n", true);
emailSendCache.put(mailAddress, userId);
}
}
@Override
public Object sendUpdateConfirmMail(MailModel mailModel) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(
lambdaQueryWrapper.eq(UserModel::getEmail, mailModel.getMailAddress())
.eq(UserModel::getStatus, 0)
.eq(UserModel::getMailVerification, 0));
if (userModel == null){
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (checkCache(mailModel.getMailAddress())) {
String code = RandomUtil.randomString(6);
emailSendCache.put(mailModel.getMailAddress(), code);
CacheHolder.VERIFICATION_MAIL.put(code, mailModel.getMailAddress());
MailUtil.send(mailModel.getMailAddress(),"your Verification code is :" + code,
"your Verification code is :" + code,false);
log.info("sendMailto:{},Verification code is :{}", mailModel.getMailAddress(),code);
}
return true;
}
public Boolean checkCache(String mailAddress){
Object ifPresent = emailSendCache.getIfPresent(mailAddress);
if (ifPresent == null) {
return true;
}else {
throw new BusinessException(ErrorCode.EMAIL_SEND_FREQUENT);
}
}
}

View File

@@ -44,3 +44,4 @@ sa-token:
activateUrl: http://192.168.1.174:8086/user/activate?token=
verificationMailUrl: http://192.168.1.174:8086/user/verification?token=
forgetPassWordUrl: http://192.168.1.174:8086/user//resetPassword/?token=

View File

@@ -3,8 +3,8 @@ host = smtp.exmail.qq.com
# 邮件服务器的SMTP端口可选默认25
port = 465
# 发件人(必须正确,否则发送失败)
from = chenfu@bilibili.so
from = niuyuxi@hanxiaokj.cn
# 用户名,默认为发件人邮箱前缀
user = chenfu@bilibili.so
pass = Yt7rWTizdAvE4dc7
user = niuyuxi@hanxiaokj.cn
pass = tF35umug9CBCBKqR
sslEnable = true

View File

@@ -44,3 +44,4 @@ sa-token:
activateUrl: http://192.168.1.174:8086/user/activate?token=
verificationMailUrl: http://192.168.1.174:8086/user/verification?token=
forgetPassWordUrl: http://192.168.1.174:8086/user//resetPassword/?token=