refactor: 重构安全架构,提取通用安全模块到common-security

- 将JwtAuthenticationFilter、JwtUtil、JwtProperties从auth服务移至common-security模块
- 新增common-security通用安全模块,提供JWT认证、权限验证等核心安全功能
- 重命名SecurityConfiguration为AuthSecurityConfiguration,使用common-security的filter
- 新增JacksonConfiguration配置类,统一JSON序列化配置
- 新增头像更新功能AvatarUpdateRequestDTO
- 移除冗余的UserLoginResponseDTO类
- 更新各服务模块的依赖配置以引入common-security模块
- 新增README.md项目说明文档

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-12 01:54:21 +08:00
parent 8bd56a6001
commit a4575cebd4
47 changed files with 704 additions and 317 deletions

View File

@@ -37,6 +37,10 @@
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-security</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
@@ -61,24 +65,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>

View File

@@ -38,6 +38,10 @@
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-security</artifactId>
</dependency>
<!-- ==================== 工具类 ==================== -->
<dependency>
@@ -70,26 +74,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- ==================== JWT ==================== -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT 和 Spring Security 依赖已在 common-security 中包含 -->
<!-- ==================== Feign 客户端 ==================== -->
<dependency>

View File

@@ -13,6 +13,6 @@ public interface UserClient {
Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount);
@GetMapping("/inner/get-by-userid")
Result<UserAuthRespDTO> getUserById(@RequestParam("userId") String userId);
Result<UserAuthRespDTO> getUserById(@RequestParam("userId") Long userId);
}

View File

@@ -1,33 +1,37 @@
package cn.meowrain.aioj.backend.auth.config;
import cn.meowrain.aioj.backend.auth.filter.JwtAuthenticationFilter;
import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Auth Service Security 配置
* 覆盖 common-security 的默认 filterChain添加 auth 服务自定义白名单
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
public class AuthSecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// Auth 服务自定义白名单
.requestMatchers("/v1/auth/**", "/oauth2/**", "/.well-known/**", "/doc.html", "/swagger-ui/**",
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico","/v1/user/email/send-code")
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico",
"/v1/user/email/send-code")
.permitAll()
.anyRequest()
.authenticated())

View File

@@ -1,29 +0,0 @@
package cn.meowrain.aioj.backend.auth.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(value = JwtPropertiesConfiguration.PREFIX)
public class JwtPropertiesConfiguration {
public static final String PREFIX = "jwt";
/**
* JWT 密钥(必须 32 字节以上)
*/
private String secret;
/**
* 过期时间(单位:毫秒)
*/
private long accessExpire; // access token TTL
/**
* 刷新令牌时间
*/
private long refreshExpire; // refresh token TTL
}

View File

@@ -56,15 +56,9 @@ public class AuthController {
}
@GetMapping("/getUserInfo")
public Result<UserAuthRespDTO> getUserInfo(@RequestHeader(value = "Authorization", required = false) String authorization) {
String token = null;
if(authorization != null && authorization.startsWith("Bearer ")){
token = authorization.substring(7);
}
if(token != null && sessionService.isTokenBlacklisted(token)) {
return Results.success(null);
}
return Results.success(authService.getUserInfo(token));
public Result<UserAuthRespDTO> getUserInfo() {
return Results.success(authService.getUserInfo());
}
}

View File

@@ -2,13 +2,14 @@ package cn.meowrain.aioj.backend.auth.dto.resp;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 用户认证响应体
*/
@Data
public class UserAuthRespDTO {
public class UserAuthRespDTO implements Serializable {
/**
* id
@@ -24,7 +25,6 @@ public class UserAuthRespDTO {
* 用户密码
*/
private String userPassword;
/**
* 开放平台id
*/
@@ -43,7 +43,7 @@ public class UserAuthRespDTO {
/**
* 用户头像
*/
private String userAvatar;
private Long userAvatar;
/**
* 用户简介
@@ -55,6 +55,16 @@ public class UserAuthRespDTO {
*/
private String userRole;
/**
* 用户邮箱
*/
private String userEmail;
/**
* 用户邮箱是否验证 0 未验证 1已验证
*/
private Integer userEmailVerified;
/**
* 创建时间
*/
@@ -65,4 +75,8 @@ public class UserAuthRespDTO {
*/
private Date updateTime;
private static final long serialVersionUID = 1L;
}

View File

@@ -1,106 +0,0 @@
package cn.meowrain.aioj.backend.auth.filter;
import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/**
* JWT认证过滤器 拦截所有请求验证JWT Token
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final AuthService authService;
private static final String TOKEN_PREFIX = "Bearer ";
private static final String HEADER_NAME = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String token = extractTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtil.isTokenValid(token)) {
Claims claims = jwtUtil.parseClaims(token);
Authentication authentication = createAuthentication(claims);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("JWT Authentication successful for user: {}", claims.getSubject());
}
else {
log.debug("No valid JWT token found in request");
}
}
catch (Exception e) {
log.error("JWT Authentication failed", e);
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
/**
* 从请求中提取JWT Token
*/
private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(HEADER_NAME);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
return bearerToken.substring(TOKEN_PREFIX.length());
}
return null;
}
/**
* 根据JWT Claims创建Authentication对象
*/
private Authentication createAuthentication(Claims claims) {
String userId = claims.getSubject();
String userName = claims.get("userName", String.class);
String role = claims.get("role", String.class);
// 创建权限列表
List<SimpleGrantedAuthority> authorities = Collections
.singletonList(new SimpleGrantedAuthority("ROLE_" + (role != null ? role : "USER")));
// 创建认证对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null,
authorities);
return authentication;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
// 跳过不需要JWT验证的路径
return path.startsWith("/v1/auth/") || path.startsWith("/doc.html") || path.startsWith("/swagger-ui/")
|| path.startsWith("/swagger-resources/") || path.startsWith("/webjars/")
|| path.startsWith("/v3/api-docs") || path.equals("/favicon.ico")
|| path.contains("/v3/api-docs");
}
}

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client;
import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2ClientService;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2SessionService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.oauth2.dto.UserInfoResponse;
import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import io.jsonwebtoken.Claims;
@@ -64,7 +64,7 @@ public class OAuth2UserInfoController {
Long userId = Long.parseLong(userIdStr);
// 4. 调用 user-service 获取用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult == null || userResult.getData() == null) {
log.error("获取用户信息失败: userId={}", userId);
throw new OAuth2Exception("server_error", "获取用户信息失败", 500);
@@ -80,7 +80,7 @@ public class OAuth2UserInfoController {
.preferredUsername(user.getUserAccount()) // 用户名
.email(null) // TODO: 从用户信息中获取邮箱
.emailVerified(false) // TODO: 从用户信息中获取邮箱验证状态
.picture(user.getUserAvatar()) // 用户头像
.picture(null) // 用户头像
.role(user.getUserRole()) // 用户角色
.build();

View File

@@ -4,7 +4,7 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.oauth2.dto.OAuth2TokenResponse;
import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import lombok.RequiredArgsConstructor;
@@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class OAuth2TokenService {
private final JwtUtil jwtUtil;
private final AuthServiceJwtUtil jwtUtil;
private final UserClient userClient;
@@ -45,7 +45,7 @@ public class OAuth2TokenService {
*/
public OAuth2TokenResponse generateTokenResponse(OAuth2Client client, Long userId, String scope, String nonce) {
// 1. 调用 user-service 获取用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult == null || userResult.getData() == null) {
throw new RuntimeException("获取用户信息失败");
}
@@ -110,7 +110,7 @@ public class OAuth2TokenService {
}
// 4. 调用 user-service 获取最新用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(Long.valueOf(userId));
if (userResult == null || userResult.getData() == null) {
throw new RuntimeException("获取用户信息失败");
}

View File

@@ -31,8 +31,7 @@ public interface AuthService {
/**
* 根据accessToken获取用户信息
* @param accessToken
* @return {@link Result<UserAuthRespDTO>}
*/
UserAuthRespDTO getUserInfo(String accessToken);
UserAuthRespDTO getUserInfo();
}

View File

@@ -5,13 +5,14 @@ import cn.hutool.crypto.digest.BCrypt;
import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.auth.config.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils;
import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.auth.dto.chains.context.UserLoginRequestParamVerifyContext;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil;
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
import cn.meowrain.aioj.backend.framework.core.exception.ServiceException;
@@ -28,7 +29,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class AuthServiceImpl implements AuthService {
private final JwtUtil jwtUtil;
private final AuthServiceJwtUtil jwtUtil;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
@@ -111,7 +112,7 @@ public class AuthServiceImpl implements AuthService {
// 再次签发新的 Access Token
// 此处你需要查用户,拿 userName, role
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult.isFail()) {
log.error("通过id查找用户失败:{}", userResult.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
@@ -156,7 +157,7 @@ public class AuthServiceImpl implements AuthService {
}
// 4. 验证用户是否存在(可选,增加安全性)
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
Result<UserAuthRespDTO> userResult = userClient.getUserById(Long.valueOf(userId));
if (userResult.isFail() || userResult.getData() == null) {
log.warn("User not found for id: {}", userId);
return false;
@@ -171,37 +172,13 @@ public class AuthServiceImpl implements AuthService {
}
@Override
public UserAuthRespDTO getUserInfo(String accessToken) {
public UserAuthRespDTO getUserInfo() {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
// 1. 参数校验
if (accessToken == null || accessToken.isBlank()) {
log.warn("Access token is null or empty");
throw new ClientException(ErrorCode.PARAMS_ERROR);
}
// 2. token 校验
if (!jwtUtil.isTokenValid(accessToken)) {
log.warn("Access token is invalid or expired");
throw new ClientException(ErrorCode.NOT_LOGIN_ERROR);
}
// 3. 解析 token
String userId;
try {
userId = jwtUtil.parseClaims(accessToken).getSubject();
} catch (Exception e) {
log.warn("Failed to parse access token", e);
throw new ClientException(ErrorCode.NOT_LOGIN_ERROR);
}
if (userId == null) {
throw new ClientException(ErrorCode.NOT_LOGIN_ERROR);
}
// 4. 查询用户信息IO 操作)
// 查询用户信息IO 操作)
Result<UserAuthRespDTO> userResult;
try {
userResult = userClient.getUserById(userId);
userResult = userClient.getUserById(currentUserId);
} catch (Exception e) {
log.error("Failed to call user service", e);
throw new ClientException(ErrorCode.SYSTEM_ERROR);

View File

@@ -0,0 +1,75 @@
package cn.meowrain.aioj.backend.auth.utils;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* Auth Service 专用的 JWT 工具类
* 包装 common-security 中的 JwtUtil提供接受 UserAuthRespDTO 的便捷方法
*/
@Component
@RequiredArgsConstructor
public class AuthServiceJwtUtil {
private final JwtUtil jwtUtil;
/**
* 生成 Access Token
* @param user 用户信息
* @return JWT Token
*/
public String generateAccessToken(UserAuthRespDTO user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("userName", user.getUserName());
claims.put("role", user.getUserRole());
return jwtUtil.generateAccessToken(user.getId(), claims);
}
/**
* 生成 Refresh Token
* @param userId 用户ID
* @return JWT Token
*/
public String generateRefreshToken(Long userId) {
return jwtUtil.generateRefreshToken(userId);
}
/**
* 生成 OIDC ID Token
* @param user 用户信息
* @param clientId 客户端IDaud
* @param nonce 防重放参数
* @return ID Token
*/
public String generateIdToken(UserAuthRespDTO user, String clientId, String nonce) {
Map<String, Object> claims = new HashMap<>();
claims.put("aud", clientId);
claims.put("name", user.getUserName());
claims.put("preferred_username", user.getUserAccount());
if (nonce != null) {
claims.put("nonce", nonce);
}
return jwtUtil.generateAccessToken(user.getId(), claims);
}
/**
* 校验 Token 是否过期
*/
public boolean isTokenValid(String token) {
return jwtUtil.isTokenValid(token);
}
/**
* 解析 Token
*/
public io.jsonwebtoken.Claims parseClaims(String token) {
return jwtUtil.parseClaims(token);
}
}

View File

@@ -1,102 +0,0 @@
package cn.meowrain.aioj.backend.auth.utils;
import cn.meowrain.aioj.backend.auth.config.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@RequiredArgsConstructor
@Component
public class JwtUtil {
private final JwtPropertiesConfiguration jwtConfig;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes());
}
/** 生成 Access Token */
public String generateAccessToken(UserAuthRespDTO user) {
long now = System.currentTimeMillis();
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("userName", user.getUserName());
claims.put("role", user.getUserRole());
return Jwts.builder()
.subject(String.valueOf(user.getId()))
.issuedAt(new Date(now))
.expiration(new Date(now + jwtConfig.getAccessExpire()))
.claims(claims)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/** 生成 Refresh Token只含 userId */
public String generateRefreshToken(Long userId) {
long now = System.currentTimeMillis();
return Jwts.builder()
.subject(String.valueOf(userId))
.issuedAt(new Date(now))
.expiration(new Date(now + jwtConfig.getRefreshExpire()))
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/** 解析 Token */
public Claims parseClaims(String token) {
return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload();
}
/** 校验 Token 是否过期 */
public boolean isTokenValid(String token) {
try {
Claims claims = parseClaims(token);
return claims.getExpiration().after(new Date());
}
catch (Exception ignored) {
return false;
}
}
/**
* 生成 OIDC ID Token
* @param user 用户信息
* @param clientId 客户端IDaud
* @param nonce 防重放参数
* @return ID Token
*/
public String generateIdToken(UserAuthRespDTO user, String clientId, String nonce) {
long now = System.currentTimeMillis();
Map<String, Object> claims = new HashMap<>();
claims.put("sub", String.valueOf(user.getId())); // Subject - 用户ID
claims.put("aud", clientId); // Audience - 客户端ID
claims.put("name", user.getUserName());
claims.put("preferred_username", user.getUserAccount());
if (nonce != null) {
claims.put("nonce", nonce); // 防重放
}
return Jwts.builder()
.issuer("http://localhost:10011/api") // TODO: 从配置读取
.subject(String.valueOf(user.getId()))
.issuedAt(new Date(now))
.expiration(new Date(now + jwtConfig.getAccessExpire()))
.claims(claims)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
}

View File

@@ -7,7 +7,7 @@ spring:
livereload:
enabled: true
server:
port: 10011
port: 18081
servlet:
context-path: /api
springdoc:
@@ -33,6 +33,7 @@ mybatis-plus:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
jwt:
enabled: true
secret: "12345678901234567890123456789012" # 至少32字节
access-expire: 900000 # 24小时
refresh-expire: 604800000 # 7天