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:
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
<artifactId>aioj-backend-common-security</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<description>AIOJ 公共安全模块 - JWT 认证和 Spring Security 配置</description>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache License, Version 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</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.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
62
aioj-backend-common/aioj-backend-common-security/pom.xml
Normal file
62
aioj-backend-common/aioj-backend-common-security/pom.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>aioj-backend-common-security</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<description>AIOJ 公共安全模块 - JWT 认证和 Spring Security 配置</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- ==================== Spring Security ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</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>
|
||||
|
||||
<!-- ==================== Lombok ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 内部模块 ==================== -->
|
||||
<dependency>
|
||||
<groupId>cn.meowrain.aioj</groupId>
|
||||
<artifactId>aioj-backend-common-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.meowrain.aioj.backend.framework.security.autoconfigure;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter;
|
||||
import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration;
|
||||
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* Spring Security 自动配置类
|
||||
*
|
||||
* 引入 aioj-backend-common-security 依赖后,会自动启用:
|
||||
* - JWT 认证过滤器
|
||||
* - Spring Security 基本配置
|
||||
*
|
||||
* 配置项(application.yml):
|
||||
* jwt:
|
||||
* enabled: true # 是否启用 JWT 认证(默认 true)
|
||||
* secret: your-secret # JWT 密钥
|
||||
* access-expire: 7200000 # Access Token 过期时间
|
||||
* refresh-expire: 604800000 # Refresh Token 过期时间
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(JwtPropertiesConfiguration.class)
|
||||
public class SecurityAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = "jwt", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public JwtUtil jwtUtil(JwtPropertiesConfiguration jwtConfig) {
|
||||
return new JwtUtil(jwtConfig);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(prefix = "jwt", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtUtil jwtUtil) {
|
||||
return new JwtAuthenticationFilter(jwtUtil);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cn.meowrain.aioj.backend.framework.security.config;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.security.autoconfigure.SecurityAutoConfiguration;
|
||||
import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@AutoConfiguration(after = SecurityAutoConfiguration.class)
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "jwt", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnBean(JwtAuthenticationFilter.class)
|
||||
public class SecurityConfiguration {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SecurityFilterChain.class)
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Swagger 文档路径
|
||||
.requestMatchers("/doc.html", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**",
|
||||
"/v3/api-docs/**", "/v3/api-docs", "/favicon.ico")
|
||||
.permitAll()
|
||||
// 用户注册、发送验证码等公开接口
|
||||
.requestMatchers("/v1/user/register", "/v1/user/email/send-code",
|
||||
"/api/v1/user/register", "/api/v1/user/email/send-code")
|
||||
.permitAll()
|
||||
// 文件访问接口(公开,用于访问图片等静态资源)
|
||||
.requestMatchers("/file/**", "/api/file/**")
|
||||
.permitAll()
|
||||
// 内部服务调用路径(Feign)- 使用具体路径匹配
|
||||
.requestMatchers("/v1/user/inner/**", "/v1/*/inner/**",
|
||||
"/api/v1/user/inner/**", "/api/v1/*/inner/**")
|
||||
.permitAll()
|
||||
// 其他请求需要认证(可由子类覆盖此配置)
|
||||
.anyRequest()
|
||||
.authenticated())
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CorsConfigurationSource.class)
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(List.of("*"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cn.meowrain.aioj.backend.framework.security.filter;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.security.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.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT 认证过滤器
|
||||
* 拦截所有请求,验证 JWT Token 并设置 SecurityContext
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
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 role = claims.get("role", String.class);
|
||||
|
||||
// 创建权限列表
|
||||
List<SimpleGrantedAuthority> authorities = Collections
|
||||
.singletonList(new SimpleGrantedAuthority("ROLE_" + (role != null ? role : "USER")));
|
||||
|
||||
// 创建认证对象(principal 为 userId 字符串)
|
||||
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("/v3/api-docs") || path.equals("/favicon.ico") || path.contains("/swagger");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.meowrain.aioj.backend.framework.security.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties(value = JwtPropertiesConfiguration.PREFIX)
|
||||
public class JwtPropertiesConfiguration {
|
||||
|
||||
public static final String PREFIX = "jwt";
|
||||
|
||||
/***
|
||||
* 开启
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* JWT 密钥(必须 32 字节以上)
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* 过期时间(单位:毫秒)
|
||||
*/
|
||||
private long accessExpire; // access token TTL
|
||||
|
||||
/**
|
||||
* 刷新令牌时间
|
||||
*/
|
||||
private long refreshExpire; // refresh token TTL
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.meowrain.aioj.backend.framework.security.utils;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration;
|
||||
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
|
||||
public class JwtUtil {
|
||||
|
||||
private final JwtPropertiesConfiguration jwtConfig;
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Access Token
|
||||
* @param userId 用户ID
|
||||
* @param additionalClaims 额外的声明(如 userName, role 等)
|
||||
*/
|
||||
public String generateAccessToken(Long userId, Map<String, Object> additionalClaims) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
if (additionalClaims != null) {
|
||||
claims.putAll(additionalClaims);
|
||||
}
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(String.valueOf(userId))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Token 中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = parseClaims(token);
|
||||
return Long.parseLong(claims.getSubject());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
cn.meowrain.aioj.backend.framework.security.autoconfigure.SecurityAutoConfiguration
|
||||
cn.meowrain.aioj.backend.framework.security.config.SecurityConfiguration
|
||||
Reference in New Issue
Block a user