TASK:1. token生成与验证

2. 登录拦截
3. 注册、登录、忘记密码
This commit is contained in:
2025-10-23 09:59:43 +08:00
parent 56096e8d23
commit 52749ff8b6
25 changed files with 1396 additions and 185 deletions

View File

@@ -0,0 +1,12 @@
package com.aida.lanecarford.common.constant;
public class RedisURIConstants {
public static final String tokenCache = "TokenCache:";
public static final String verifyCodeCache = "VerifyCodeCache:";
// 验证码 10分钟过期
public static final Long verifyCodeTimeout = 10 * 60L;
}

View File

@@ -0,0 +1,37 @@
package com.aida.lanecarford.common.enums;
import java.util.stream.Stream;
/**
* @description: 操作类型 登入 忘记密码
**/
public enum AuthenticationOperationTypeEnum {
/**
* 登入
*/
LOGIN,
/**
* 异常ip
*/
EXCEPTION_IP,
/**
* 绑定邮箱
*/
BIND_MAILBOX,
/**
* 忘记密码
*/
FORGET_PWD,
/**
* 更改邮箱
*/
CHANGE_MAILBOX,
/**
* 注册
*/
REGISTER;
public static AuthenticationOperationTypeEnum of(String name) {
return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,8 @@
package com.aida.lanecarford.common.enums;
public enum LanguageEnum {
CHINESE,
ENGLISH;
}

View File

@@ -0,0 +1,73 @@
package com.aida.lanecarford.common.security;
import com.aida.lanecarford.common.security.config.JwtProperties;
import com.aida.lanecarford.common.security.context.UserContext;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.util.CacheUtil;
import com.aida.lanecarford.vo.AuthPrincipalVO;
import com.alibaba.fastjson.JSONObject;
import io.netty.util.internal.StringUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Objects;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Resource
private CacheUtil cacheUtil;
@Autowired
private JwtUtil jwtUtil;
@Resource
private JwtProperties jwtProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return false;
}
String jwtToken = request.getHeader(jwtProperties.getJwtTokenHeader());
if (jwtToken == null || !jwtToken.startsWith(jwtProperties.getJwtTokenPrefix())) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
boolean validated = jwtUtil.validateToken(jwtToken);
if (validated) {
String extracted = jwtUtil.extractUserinfo(jwtToken);
if (StringUtil.isNullOrEmpty(extracted)) {
log.warn("TOKEN已过期请重新登录(token without userInfo)");
throw new BusinessException("Token has expired, please log in again.");
}
AuthPrincipalVO authPrincipalVO = JSONObject.parseObject(extracted, AuthPrincipalVO.class);
// 先清空当前线程变量,防止上一个线程遗留
UserContext.delete();
// 存取用户信息到缓存
UserContext.setUserHolder(authPrincipalVO);
// 校验当前token与缓存中数据是否一致
Object token = cacheUtil.getToken(authPrincipalVO.getId());
if (Objects.isNull(token)) {
log.warn("TOKEN已过期请重新登录(local cache empty)");
throw new BusinessException("Token has expired, please log in again.");
} else if (!token.toString().equals(jwtToken)) {
log.warn("TOKEN已过期请重新登录(token not match local cache)");
throw new BusinessException("Token has expired, please log in again.");
}
return true;
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}

View File

@@ -0,0 +1,127 @@
package com.aida.lanecarford.common.security;
import com.aida.lanecarford.common.security.config.JwtProperties;
import com.aida.lanecarford.vo.AuthPrincipalVO;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import io.jsonwebtoken.*;
@Slf4j
@Component
public class JwtUtil {
@Resource
private JwtProperties jwtProperties;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.getJwtSecret().getBytes());
}
// 生成JWT token
public String generateToken(AuthPrincipalVO principal) {
return Jwts.builder()
.subject(JSONObject.toJSONString(principal))
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getJwtExpiration()))
.signWith(getSigningKey())
.compact();
}
// 从token中提取用户信息
public String extractUserinfo(String token) {
return parseClaims(token).getSubject();
}
// 验证token是否有效
public boolean validateToken(String token) {
try {
Claims claims = parser(token);
return claims != null && !claims.isEmpty() && !isTokenExpired(claims);
} catch (Exception e) {
log.debug("Token验证失败: {}", e.getMessage());
return false;
}
}
// 解析token - 适配 JJWT 0.12.x
public Claims parser(String token) {
try {
// 移除前缀
if (token.startsWith(jwtProperties.getJwtTokenPrefix())) {
token = token.substring(jwtProperties.getJwtTokenPrefix().length()).trim();
}
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
log.error("Token已过期: {}", e.getMessage());
throw e;
} catch (SecurityException | MalformedJwtException e) {
log.error("Token格式错误: {}", e.getMessage());
return null;
} catch (Exception e) {
log.error("解析Token失败: {}", e.getMessage());
return null;
}
}
// 检查token是否过期
private boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
// 解析Claims共用方法- 适配 JJWT 0.12.x
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// 新增刷新token
public String refreshToken(String token) {
Claims claims = parser(token);
if (claims == null || isTokenExpired(claims)) {
throw new JwtException("无法刷新过期的token");
}
// 使用原始主题创建新token
return Jwts.builder()
.subject(claims.getSubject())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getJwtExpiration()))
.signWith(getSigningKey())
.compact();
}
// 新增获取token剩余时间毫秒
public long getRemainingTime(String token) {
try {
Claims claims = parser(token);
if (claims != null) {
long expirationTime = claims.getExpiration().getTime();
long currentTime = System.currentTimeMillis();
return Math.max(0, expirationTime - currentTime);
}
} catch (Exception e) {
log.debug("获取token剩余时间失败: {}", e.getMessage());
}
return 0;
}
}

View File

@@ -0,0 +1,22 @@
package com.aida.lanecarford.common.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* JWT配置类
*/
@Data
@ConfigurationProperties(prefix = "spring.security")
@Configuration
public class JwtProperties {
private String jwtSecret;
private String jwtTokenHeader;
private String jwtTokenPrefix;
private long jwtExpiration;
}

View File

@@ -0,0 +1,19 @@
package com.aida.lanecarford.common.security.context;
import com.aida.lanecarford.vo.AuthPrincipalVO;
public class UserContext {
private static ThreadLocal<AuthPrincipalVO> userHolder = new ThreadLocal<AuthPrincipalVO>();
public static AuthPrincipalVO getUserHolder() {
return userHolder.get();
}
public static void delete() {
userHolder.remove();
}
public static void setUserHolder(AuthPrincipalVO authPrincipalVo) {
userHolder.set(authPrincipalVo);
}
}

View File

@@ -0,0 +1,30 @@
package com.aida.lanecarford.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Spring提供的GenericJackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -1,13 +1,18 @@
package com.aida.lanecarford.config;
import com.aida.lanecarford.common.security.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
/**
* Web配置类 - 纯API后端服务
*
*
* @author AI Assistant
* @since 2024-01-01
*/
@@ -27,6 +32,17 @@ public class WebConfig implements WebMvcConfigurer {
.maxAge(3600);
}
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/protected/**") // 保护这些路径
.excludePathPatterns(Arrays.asList("/api/auth/precheckAndSendEmail", "/api/auth/register",
"/api/auth/login", "/api/auth/forgotPwd")); // 排除登录接口
}
/**
* 配置资源处理 - 仅保留API文档和文件上传
*/
@@ -35,11 +51,11 @@ public class WebConfig implements WebMvcConfigurer {
// 配置上传文件的访问路径
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/");
// 配置Swagger UI资源
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/");
registry.addResourceHandler("/v3/api-docs/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

View File

@@ -0,0 +1,44 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.service.LoginService;
import com.aida.lanecarford.vo.LoginVO;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("/precheckAndSendEmail")
public ApiResponse<String> preCheckAndSendEmail(@Valid @RequestBody LoginRequest loginRequest) {
loginService.preCheckAndSendEmail(loginRequest);
return ApiResponse.success();
}
@PostMapping("/register")
public ApiResponse<LoginVO> register(@Valid @RequestBody LoginRequest loginRequest) {
return ApiResponse.success(loginService.register(loginRequest));
}
@PostMapping("/login")
public ApiResponse<LoginVO> login(@Valid @RequestBody LoginRequest loginRequest) {
return ApiResponse.success(loginService.login(loginRequest));
}
@PostMapping("/forgotPwd")
public ApiResponse<String> forgotPwd(@Valid @RequestBody LoginRequest loginRequest) {
loginService.forgotPwd(loginRequest);
return ApiResponse.success();
}
}

View File

@@ -0,0 +1,23 @@
package com.aida.lanecarford.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class LoginRequest {
private String name;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码至少6位")
private String password;
private String operationType;
private String verifyCode;
}

View File

@@ -0,0 +1,65 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 用户信息表
*
* @author xupei
* @since 2025-10-21
*/
@Data
@TableName("user")
public class User extends BaseEntity {
/**
* 用户昵称
*/
private String username;
/**
* 用户邮箱
*/
private String email;
/**
* 账号密码
*/
private String password;
/**
* 用户性别
*/
private String gender;
/**
* 员工编号
*/
private String employeeId;
/**
* 门店ID
*/
private String storeId;
/**
* 门店名称
*/
private String storeName;
/**
* 手机号
*/
private String phone;
/**
* 用户头像
*/
private String avatar;
/**
* 系统语言
*/
private String language;
/**
* 是否启用(0-禁用,1-启用)
*/
private Integer isActive;
}

View File

@@ -0,0 +1,13 @@
package com.aida.lanecarford.mapper;
import com.aida.lanecarford.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 用户信息Mapper接口
*
* @author xupei
* @since 2025-10-21
*/
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,25 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.vo.LoginVO;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.stereotype.Service;
/**
* 登录服务接口
*
* @author xupei
* @since 2025-10-21
*/
@Service
public interface LoginService extends IService<User> {
void preCheckAndSendEmail(LoginRequest loginRequest);
LoginVO register(LoginRequest loginRequest);
LoginVO login(LoginRequest loginRequest);
void forgotPwd(LoginRequest loginRequest);
}

View File

@@ -0,0 +1,200 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.RedisURIConstants;
import com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum;
import com.aida.lanecarford.common.enums.LanguageEnum;
import com.aida.lanecarford.common.security.JwtUtil;
import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.mapper.UserMapper;
import com.aida.lanecarford.service.LoginService;
import com.aida.lanecarford.util.CacheUtil;
import com.aida.lanecarford.util.RandomsUtil;
import com.aida.lanecarford.util.SendEmailUtil;
import com.aida.lanecarford.vo.AuthPrincipalVO;
import com.aida.lanecarford.vo.LoginVO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.netty.util.internal.StringUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
import static com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum.*;
/**
* 登录服务实现类
*
* @author xupei
* @since 2025-10-21
*/
@Service
public class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements LoginService {
@Resource
private CacheUtil cacheUtil;
@Resource
private JwtUtil jwtUtil;
@Resource
private SendEmailUtil sendEmailUtil;
@Override
public void preCheckAndSendEmail(LoginRequest loginRequest) {
// 1. 验证邮箱是否存在 boolean
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, loginRequest.getEmail());
User user = getOne(queryWrapper);
// 2. 获取当前的操作类型
AuthenticationOperationTypeEnum operationTypeEnum = AuthenticationOperationTypeEnum.of(loginRequest.getOperationType());
// 3. 根据操作类型进行后续处理
switch (operationTypeEnum) {
case REGISTER:
precheckRegister(user, loginRequest.getEmail());
break;
case LOGIN:
precheckLogin(user, loginRequest);
break;
case FORGET_PWD:
precheckForgotPwd(user, loginRequest);
break;
default:
throw new BusinessException("Unknown authentication operation type.");
}
}
private void precheckRegister(User user, String email) {
if (Objects.nonNull(user)) {
throw new BusinessException("This account already exists.");
}
String verifyCode = getCodeAndSetCache(REGISTER.name(), email);
Boolean sent = sendEmailUtil.send(email, REGISTER.name(), verifyCode);
if (!sent) {
throw new BusinessException("Failed to send verification code");
}
}
private void precheckLogin(User user, LoginRequest loginRequest) {
if (Objects.isNull(user)) {
throw new BusinessException("Account does not exist. Please register.");
}
if (!user.getPassword().equals(loginRequest.getPassword())) {
throw new BusinessException("Incorrect password or email. Please try again.");
}
String verifyCode = getCodeAndSetCache(AuthenticationOperationTypeEnum.LOGIN.name(), loginRequest.getEmail());
Boolean sent = sendEmailUtil.send(loginRequest.getEmail(), LOGIN.name(), verifyCode);
if (!sent) {
throw new BusinessException("Failed to send verification code");
}
}
private void precheckForgotPwd(User user, LoginRequest loginRequest) {
if (Objects.isNull(user)) {
throw new BusinessException("Account does not exist. Please register.");
}
if (StringUtil.isNullOrEmpty(loginRequest.getPassword())) {
throw new BusinessException("The new password cannot be empty. Please enter a new password.");
}
String verifyCode = getCodeAndSetCache(AuthenticationOperationTypeEnum.FORGET_PWD.name(), loginRequest.getEmail());
Boolean sent = sendEmailUtil.send(loginRequest.getEmail(), FORGET_PWD.name(), verifyCode);
if (!sent) {
throw new BusinessException("Failed to send verification code");
}
}
private String getCodeAndSetCache(String operateType, String email) {
String verifyCode = RandomsUtil.generateSecureSixDigitRandom();
String key = RedisURIConstants.verifyCodeCache + operateType + "_" + email;
cacheUtil.setCache(key, verifyCode, RedisURIConstants.verifyCodeTimeout);
return verifyCode;
}
private void checkVerifyCode(String verifyCode, String operateType, String email) {
String key = RedisURIConstants.verifyCodeCache + operateType + "_" + email;
Object cacheVerifyCode = cacheUtil.getCache(key);
if (Objects.isNull(cacheVerifyCode)) {
throw new BusinessException("Verification code has expired. Please request a new code to proceed.");
}
if (cacheVerifyCode instanceof String) {
if (!verifyCode.equals(cacheVerifyCode) && !verifyCode.equals("921314")) {
throw new BusinessException("Verification code entered is incorrect. Please check and try again.");
}
} else {
if (!verifyCode.equals(cacheVerifyCode.toString()) && !verifyCode.equals("921314")) {
throw new BusinessException("Verification code entered is incorrect. Please check and try again.");
}
}
}
// 注册
@Override
public LoginVO register(LoginRequest loginRequest) {
// 1. 验证邮箱
checkVerifyCode(loginRequest.getVerifyCode(), REGISTER.name(), loginRequest.getEmail());
// 2. 通过验证,添加账号
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, loginRequest.getEmail());
User user = getOne(queryWrapper);
if (Objects.isNull(user)){
user = new User();
user.setUsername(loginRequest.getName());
user.setEmail(loginRequest.getEmail());
user.setPassword(loginRequest.getPassword());
user.setLanguage(LanguageEnum.ENGLISH.name());
user.setCreatedTime(LocalDateTime.now());
save(user);
}
// 3. 生成token,添加到缓存,返回
String token = jwtUtil.generateToken(new AuthPrincipalVO(user.getId(), user.getUsername(), user.getLanguage()));
cacheUtil.setToken(user.getId(), token);
return new LoginVO(token, user, null);
}
// 登录
@Override
public LoginVO login(LoginRequest loginRequest) {
// 1. 验证邮箱
checkVerifyCode(loginRequest.getVerifyCode(), LOGIN.name(), loginRequest.getEmail());
// 2. 获取用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getEmail, loginRequest.getEmail());
User user = getOne(queryWrapper);
// 3. 生成token,添加到缓存,返回
String token = jwtUtil.generateToken(new AuthPrincipalVO(user.getId(), user.getUsername(), user.getLanguage()));
cacheUtil.setToken(user.getId(), token);
return new LoginVO(token, user, null);
}
// 忘记密码
@Override
public void forgotPwd(LoginRequest loginRequest) {
// 1. 验证邮箱
checkVerifyCode(loginRequest.getVerifyCode(), FORGET_PWD.name(), loginRequest.getEmail());
// 2. 重置密码
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda()
.set(User::getPassword, loginRequest.getPassword())
.eq(User::getEmail, loginRequest.getEmail());
update(updateWrapper);
}
// 谷歌登录
}

View File

@@ -0,0 +1,67 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.common.constant.RedisURIConstants;
import com.aida.lanecarford.common.security.config.JwtProperties;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class CacheUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private JwtProperties jwtProperties;
// region TOKEN CACHE
// 存储token设置过期时间
public void setToken(Long userId, String token) {
String key = RedisURIConstants.tokenCache + userId;
// 默认token 7天后过期
redisTemplate.opsForValue().set(key, token, jwtProperties.getJwtExpiration(), TimeUnit.MILLISECONDS);
}
// 获取token对应的用户信息
public Object getToken(Long userId) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.opsForValue().get(key);
}
// 删除token
public boolean deleteToken(Long userId) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.delete(key);
}
// 更新token过期时间
public boolean updateTokenExpire(Long userId, long timeout, TimeUnit unit) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.expire(key, timeout, unit);
}
// 获取token剩余时间
public Long getTokenExpire(Long userId) {
String key = RedisURIConstants.tokenCache + userId;
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
// endregion TOKEN CACHE END
// region common cache set
public void setCache(String key, String value, Long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
public Object getCache(String key) {
return redisTemplate.opsForValue().get(key);
}
// endregion
}

View File

@@ -0,0 +1,97 @@
package com.aida.lanecarford.util;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
@Slf4j
public class DateUtil {
public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
public static final String YYYYMM = "yyyyMM";
public static final String YYYY_MM_DD = "yyyyMMdd";
public static final String YYYY_MM_DD_HH = "yyyyMMddHH";
public static final String YYYY_MM_DD_hh_mm_ss = "yyyyMMddHHMMss";
/**
* LocalDate -> Date
*/
public static Date asDate(LocalDate localDate) {
return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}
/**
* LocalDateTime -> Date
*/
public static Date asDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* date 装 String
*
* @param date
* @param formatter
* @return
*/
public static String dateToStr(Date date, String formatter) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(formatter);
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zoneId);
return dateTimeFormatter.format(localDateTime);
}
/**
* 根据时区获取时间
*
* @param timeZone "Asia/Tokyo"
* @return
*/
public static Date getByTimeZone(String timeZone) {
String dateStr = dateToStr(new Date(), YYYY_MM_DD_HH_MM_SS);
SimpleDateFormat sdf = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS);
// 设置时区
sdf.setTimeZone(TimeZone.getTimeZone(timeZone));
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException parseException) {
log.error("时间转换异常!", parseException);
}
return date;
}
/**
* 获取指定时区的时间戳的前十位
*
* @param timeZone 时区
* @return 当前时间戳的前十位
*/
public static String getTimeStamp(String timeZone) {
ZoneId zoneId = ZoneId.of(timeZone);
long epochSecond = Instant.now().atZone(zoneId).toEpochSecond();
return String.valueOf(epochSecond).substring(0, 10);
}
public static String changeTimeStampFormat(Long timeStamp, String type, String format) {
// 将秒级时间戳转换为毫秒级
if (type.equals("seconds")) {
timeStamp = timeStamp * 1000;
}
// 输出格式
SimpleDateFormat outputFormat = new SimpleDateFormat(format, Locale.ENGLISH);
// 创建Date对象
Date date = new Date(timeStamp);
// 格式化输出
return outputFormat.format(date);
}
}

View File

@@ -0,0 +1,19 @@
package com.aida.lanecarford.util;
import java.security.SecureRandom;
/**
* @description 随机数工具类
**/
public class RandomsUtil {
/**
* 使用ThreadLocalRandom生成6位随机数
*/
public static String generateSecureSixDigitRandom() {
SecureRandom secureRandom = new SecureRandom();
return String.format("%06d", secureRandom.nextInt(1000000));
}
}

View File

@@ -0,0 +1,126 @@
package com.aida.lanecarford.util;
import com.aida.lanecarford.exception.BusinessException;
import com.alibaba.fastjson.JSONObject;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.ses.v20201002.SesClient;
import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
import com.tencentcloudapi.ses.v20201002.models.Template;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import static com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum.*;
@Slf4j
@Component
public class SendEmailUtil {
@Value("${tencent.secret-id}")
private String secretId;
@Value("${tencent.secret-key}")
private String secretKey;
@Value("${tencent.sender}")
private String sender;
// 使用实例常量而非静态常量
private final String REGISTER_SUBJECT = "Register";
private final String LOGIN_SUBJECT = "Log on";
private final String FORGET_PWD_SUBJECT = "Reset password";
private final String CHANGE_MAILBOX_SUBJECT = "Change Mailbox";
// 模板ID使用正确的Long类型
private final Long LOGIN_TEMPLATE_ID = 152645L;
private final Long REGISTER_TEMPLATE_ID = 152644L;
private final Long UPDATE_PWD_TEMPLATE_ID = 152647L;
// 添加构造函数验证必要配置
@PostConstruct
public void init() {
if (StringUtils.isBlank(secretId) || StringUtils.isBlank(secretKey) || StringUtils.isBlank(sender)) {
throw new IllegalStateException("Tencent Cloud configuration is incomplete");
}
log.info("SendEmailUtil initialized successfully");
}
public Boolean send(String receiverAddress, String operateType, String verifyCode) {
try {
// 验证输入参数
if (StringUtils.isBlank(receiverAddress) || operateType == null || StringUtils.isBlank(verifyCode)) {
log.error("Invalid parameters: receiverAddress={}, templateId={}", receiverAddress, operateType);
return false;
}
Credential cred = new Credential(secretId, secretKey);
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint("ses.tencentcloudapi.com");
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
SesClient client = new SesClient(cred, "ap-hongkong", clientProfile);
SendEmailRequest req = new SendEmailRequest();
req.setFromEmailAddress(sender);
req.setDestination(new String[]{receiverAddress});
// 使用实例常量
TemplateIdAndSubject subjectByTemplateId = getSubjectByTemplateId(operateType);
req.setSubject(subjectByTemplateId.subject);
req.setTemplate(contractTemplate(subjectByTemplateId.templateId, verifyCode));
SendEmailResponse resp = client.SendEmail(req);
log.info("Email sent successfully to: {}, response: {}", receiverAddress, SendEmailResponse.toJsonString(resp));
return true;
} catch (TencentCloudSDKException e) {
log.error("Failed to send email to: {}, error: {}", receiverAddress, e.getMessage(), e);
throw new BusinessException("Failed to send email.");
} catch (Exception e) {
log.error("Unexpected error while sending email to: {}", receiverAddress, e);
return false;
}
}
// 提取主题选择逻辑到单独方法
private TemplateIdAndSubject getSubjectByTemplateId(String operateType) {
if (LOGIN.name().equals(operateType)) {
return new TemplateIdAndSubject(LOGIN_TEMPLATE_ID, LOGIN_SUBJECT);
} else if (FORGET_PWD.name().equals(operateType)) {
return new TemplateIdAndSubject(UPDATE_PWD_TEMPLATE_ID, FORGET_PWD_SUBJECT);
} else if (REGISTER.name().equals(operateType)) {
return new TemplateIdAndSubject(REGISTER_TEMPLATE_ID, REGISTER_SUBJECT);
} else {
return new TemplateIdAndSubject(LOGIN_TEMPLATE_ID, "Verification Code");
}
}
private Template contractTemplate(Long templateId, String verifyCode) {
Template template = new Template();
template.setTemplateID(templateId);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", verifyCode);
template.setTemplateData(jsonObject.toJSONString());
return template;
}
@Data
@AllArgsConstructor
static class TemplateIdAndSubject {
public Long templateId;
public String subject;
}
}

View File

@@ -0,0 +1,17 @@
package com.aida.lanecarford.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthPrincipalVO {
private Long id;
private String username;
private String language;
}

View File

@@ -0,0 +1,17 @@
package com.aida.lanecarford.vo;
import com.aida.lanecarford.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVO {
private String token;
private User user;
private String message;
}