TASK:1. token生成与验证
2. 登录拦截 3. 注册、登录、忘记密码
This commit is contained in:
304
pom.xml
304
pom.xml
@@ -1,46 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.aida</groupId>
|
||||
<artifactId>lanecarford</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>lanecarford</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.aida</groupId>
|
||||
<artifactId>lanecarford</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>lanecarford</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--minio-->
|
||||
<dependency>
|
||||
@@ -50,55 +50,53 @@
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- MyBatis-Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
<!-- MyBatis-Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
</dependency>
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Actuator -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Actuator -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!-- Swagger for API Documentation -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.6.0</version>
|
||||
</dependency>
|
||||
<!-- Swagger for API Documentation -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Google 认证库 -->
|
||||
<dependency>
|
||||
@@ -134,53 +132,111 @@
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<!-- JJWT API -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JJWT 实现 -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson 支持 -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- tencent-cloud -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-ses</artifactId>
|
||||
<version>3.1.572</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<!-- 本地开发环境 -->
|
||||
<id>dev</id>
|
||||
<properties>
|
||||
<profiles.active>dev</profiles.active>
|
||||
</properties>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
<profile>
|
||||
<!-- 生产环境 -->
|
||||
<id>prod</id>
|
||||
<properties>
|
||||
<profiles.active>prod</profiles.active>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.aida.lanecarford.common.enums;
|
||||
|
||||
public enum LanguageEnum {
|
||||
|
||||
CHINESE,
|
||||
|
||||
ENGLISH;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
127
src/main/java/com/aida/lanecarford/common/security/JwtUtil.java
Normal file
127
src/main/java/com/aida/lanecarford/common/security/JwtUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/aida/lanecarford/config/RedisConfig.java
Normal file
30
src/main/java/com/aida/lanecarford/config/RedisConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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/");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
23
src/main/java/com/aida/lanecarford/dto/LoginRequest.java
Normal file
23
src/main/java/com/aida/lanecarford/dto/LoginRequest.java
Normal 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;
|
||||
}
|
||||
65
src/main/java/com/aida/lanecarford/entity/User.java
Normal file
65
src/main/java/com/aida/lanecarford/entity/User.java
Normal 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;
|
||||
}
|
||||
13
src/main/java/com/aida/lanecarford/mapper/UserMapper.java
Normal file
13
src/main/java/com/aida/lanecarford/mapper/UserMapper.java
Normal 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> {
|
||||
}
|
||||
25
src/main/java/com/aida/lanecarford/service/LoginService.java
Normal file
25
src/main/java/com/aida/lanecarford/service/LoginService.java
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 谷歌登录
|
||||
}
|
||||
67
src/main/java/com/aida/lanecarford/util/CacheUtil.java
Normal file
67
src/main/java/com/aida/lanecarford/util/CacheUtil.java
Normal 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
|
||||
|
||||
}
|
||||
97
src/main/java/com/aida/lanecarford/util/DateUtil.java
Normal file
97
src/main/java/com/aida/lanecarford/util/DateUtil.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
19
src/main/java/com/aida/lanecarford/util/RandomsUtil.java
Normal file
19
src/main/java/com/aida/lanecarford/util/RandomsUtil.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
126
src/main/java/com/aida/lanecarford/util/SendEmailUtil.java
Normal file
126
src/main/java/com/aida/lanecarford/util/SendEmailUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/aida/lanecarford/vo/AuthPrincipalVO.java
Normal file
17
src/main/java/com/aida/lanecarford/vo/AuthPrincipalVO.java
Normal 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;
|
||||
}
|
||||
17
src/main/java/com/aida/lanecarford/vo/LoginVO.java
Normal file
17
src/main/java/com/aida/lanecarford/vo/LoginVO.java
Normal 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;
|
||||
}
|
||||
82
src/main/resources/application-dev.yml
Normal file
82
src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,82 @@
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
# Spring Boot 应用配置
|
||||
spring:
|
||||
# 应用基本信息
|
||||
application:
|
||||
name: lanecarford-ai-styling-assistant
|
||||
|
||||
# 数据源配置 - MySQL
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/lanecrawford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# HikariCP 连接池配置
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
idle-timeout: 300000
|
||||
max-lifetime: 1200000
|
||||
connection-timeout: 20000
|
||||
|
||||
# redis 配置
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
database: 3
|
||||
# password: Aidlab
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: 5
|
||||
security:
|
||||
jwtSecret: TXacaath8k63fQMAkfuRk3s5GTZyjRpS
|
||||
jwtTokenHeader: Authorization
|
||||
jwtTokenPrefix: Bearer-
|
||||
jwtExpiration: 604800000
|
||||
|
||||
# MyBatis-Plus 配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
|
||||
# Swagger 文档配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
enabled: true
|
||||
|
||||
# MinIO 对象存储配置
|
||||
minio:
|
||||
endpoint: https://www.minio-api.aida.com.hk
|
||||
access-key: admin
|
||||
secret-key: Aidlab123123!
|
||||
bucket-name: lanecarford
|
||||
# 文件访问URL前缀
|
||||
url-prefix: ${minio.endpoint}/${minio.bucket-name}/
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.aida.lanecarford: DEBUG
|
||||
|
||||
tencent:
|
||||
secret-id: AKID52lRwDIBsLaZLtDI9m9LJMAj36wYw50i
|
||||
secret-key: XqujLlywhHfrqcCYfYVHtNgmeIiwxkKf
|
||||
sender: info@aida.com.hk
|
||||
|
||||
72
src/main/resources/application-prod.yml
Normal file
72
src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
# Spring Boot 应用配置
|
||||
|
||||
spring:
|
||||
# 应用基本信息
|
||||
application:
|
||||
name: lanecarford-ai-styling-assistant
|
||||
|
||||
# 数据源配置 - MySQL
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/lanecarford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# HikariCP 连接池配置
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
idle-timeout: 300000
|
||||
max-lifetime: 1200000
|
||||
connection-timeout: 20000
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: 172.31.11.32
|
||||
port: 6379
|
||||
database: 3
|
||||
password: Aidlab
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: 5
|
||||
security:
|
||||
jwtSecret: JWTSECRET
|
||||
jwtTokenHeader: Authorization
|
||||
jwtTokenPrefix: Bearer-
|
||||
jwtExpiration: 604800000
|
||||
|
||||
# MyBatis-Plus 配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
# Swagger 文档配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
enabled: true
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.aida.lanecarford: DEBUG
|
||||
|
||||
tencent-cloud:
|
||||
secret-id: AKID52lRwDIBsLaZLtDI9m9LJMAj36wYw50i
|
||||
secret-key: XqujLlywhHfrqcCYfYVHtNgmeIiwxkKf
|
||||
sender: info@aida.com.hk
|
||||
@@ -1,59 +1,3 @@
|
||||
# Spring Boot 应用配置
|
||||
spring:
|
||||
# 应用基本信息
|
||||
application:
|
||||
name: lanecarford-ai-styling-assistant
|
||||
|
||||
# 数据源配置 - MySQL
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/lanecarford?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# HikariCP 连接池配置
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
idle-timeout: 300000
|
||||
max-lifetime: 1200000
|
||||
connection-timeout: 20000
|
||||
|
||||
|
||||
|
||||
# MyBatis-Plus 配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
# Swagger 文档配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
enabled: true
|
||||
|
||||
# MinIO 对象存储配置
|
||||
minio:
|
||||
endpoint: https://www.minio-api.aida.com.hk
|
||||
access-key: admin
|
||||
secret-key: Aidlab123123!
|
||||
bucket-name: lanecarford
|
||||
# 文件访问URL前缀
|
||||
url-prefix: ${minio.endpoint}/${minio.bucket-name}/
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.aida.lanecarford: DEBUG
|
||||
profiles:
|
||||
active: dev
|
||||
Reference in New Issue
Block a user