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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("获取用户信息失败");
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ public interface AuthService {
|
||||
|
||||
/**
|
||||
* 根据accessToken获取用户信息
|
||||
* @param accessToken
|
||||
* @return {@link Result<UserAuthRespDTO>}
|
||||
*/
|
||||
UserAuthRespDTO getUserInfo(String accessToken);
|
||||
UserAuthRespDTO getUserInfo();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 客户端ID(aud)
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 客户端ID(aud)
|
||||
* @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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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天
|
||||
Reference in New Issue
Block a user