SecurityConfig 类描述
SecurityConfig 是 Spring Security6 的 Web 安全核心配置类,标注 @EnableWebSecurity、@EnableMethodSecurity,构建 SecurityFilterChain 安全过滤链,专门负责 /api/**、/actuator/** 接口的安全防护,采用无状态 JWT/OAuth2 资源服务器模式 ,对接自定义动态权限管理器 ApiAuthorizationManager,前后端分离 RESTful 接口安全方案。
核心职责
- 安全匹配范围 :过滤链仅拦截
/api/**、/actuator/**请求,只处理后端 API 接口安全; - HTTP 安全响应头加固:配置防嵌套 iframe、XSS 防护、Referrer 策略、权限策略等响应头,提升浏览器侧安全;
- CSRF 防护配置 :使用 Cookie 存储 CSRF Token,Cookie 配置域名、SameSite=None、Secure,支持前端 JS 读取 token;放行
/api/auth/csrf接口跳过 CSRF 校验; - 放行白名单接口 :静态资源、
/api/auth/**、/error、/api/cms/**直接放行无需鉴权;其余全部接口交给自定义apiAuthorizationManager做数据库动态权限鉴权; - OAuth2 资源服务器配置 :自定义
CommonBearerTokenResolver,多渠道解析 Token:Header、Cookie、URL 查询参数、表单参数均可携带 token;设置忽略 token 解析路径;注入自定义未认证、无权限异常处理器;使用外部AuthenticationManagerResolver做 token 认证解析; - 会话与安全上下文 :
STATELESS无状态模式,不创建 session;安全上下文存储在 RequestAttribute,不依赖 session;禁用 requestCache,关闭登录跳转缓存; - 登录表单关闭:禁用原生 formLogin 表单登录,完全使用 REST JSON 自定义处理器;
- 自定义过滤器植入 :将
UserFilter放在AnonymousAuthenticationFilter之后执行,做用户预处理; - 全局异常处理 :统一接入
RestAuthenticationEntryPoint(未登录)、RestAccessDeniedHandler(权限不足),输出 JSON 格式错误,而非页面跳转; - 开启方法级安全 :
@EnableMethodSecurity(mode = AdviceMode.ASPECTJ),支持@PreAuthorize注解做方法粒度权限控制。
关键组件依赖
ApiAuthorizationManager:自定义动态 URL 权限管理器,数据库驱动接口鉴权;UserFilter:自定义用户预处理过滤器;CommonBearerTokenResolver:多来源 token 解析器;RestxxxHandler:全套 REST 风格安全异常处理器,返回 JSON 响应;AuthenticationManagerResolver:动态解析认证管理器,用于 token 校验。
技术特征
- proxyBeanMethods=false,减少 Spring 代理,提升启动性能;开启 webSecurity debug 调试;
- CSRF 采用 Cookie 模式适配前后端分离,前端 JS 读取 Cookie 里的 csrfToken 放入请求头;
- 完全无状态,不依赖服务端 session,适配 JWT 令牌体系;
- 两层鉴权:URL 层由
ApiAuthorizationManager动态数据库权限;业务层可使用@PreAuthorize方法注解; - oauth2 资源服务器不局限 Header 的 Authorization,支持 cookie、query、form 多种方式传 token,适配多种客户端场景。
备注潜在关注点
- CSRF Cookie 写死
domain("kongjs.com"),部署多环境需要注意环境适配;Secure=true 要求必须 HTTPS;SameSite=None 配合跨域; @EnableMethodSecurity使用 ASPECTJ 模式,需要编译期 / 加载期织入,否则方法注解不会生效;- 过滤链 securityMatcher 限定只处理 /api/**,非该前缀接口不走此套安全规则;
- actuator 端点纳入此安全链管控,会受鉴权逻辑约束;
- 注释掉 oneTimeTokenLogin 一次性令牌登录能力,可按需开启。
整体架构:Token 多渠道解析 → OAuth2 资源服务器校验令牌 → UserFilter 后置处理 → ApiAuthorizationManager 完成 URL 动态权限校验 → 业务层可叠加方法注解鉴权;全部异常输出 JSON,无页面跳转,适配前后端分离。
java
package com.kongjs.smo.common.security.config;
import com.kongjs.smo.common.security.authorization.ApiAuthorizationManager;
import com.kongjs.smo.common.security.filter.UserFilter;
import com.kongjs.smo.common.security.handler.RestAccessDeniedHandler;
import com.kongjs.smo.common.security.handler.RestAuthenticationEntryPoint;
import com.kongjs.smo.common.security.handler.RestAuthenticationFailureHandler;
import com.kongjs.smo.common.security.handler.RestAuthenticationSuccessHandler;
import com.kongjs.smo.common.security.resolver.CommonBearerTokenResolver;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.security.autoconfigure.web.servlet.PathRequest;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import java.time.Duration;
@EnableWebSecurity(debug = true)
@EnableMethodSecurity(mode = AdviceMode.ASPECTJ)
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Resource
private UserFilter userFilter;
@Resource
private RestAccessDeniedHandler restAccessDeniedHandler;
@Resource
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Resource
private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;
@Resource
private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
@Resource
private ApiAuthorizationManager apiAuthorizationManager;
@Resource
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) {
http
.securityMatcher("/api/**", "/actuator/**")
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.contentTypeOptions(Customizer.withDefaults())
.referrerPolicy(referrerPolicy -> referrerPolicy.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
.permissionsPolicyHeader(Customizer.withDefaults())
)
.csrf(csrf -> {
CookieCsrfTokenRepository csrfTokenRepository = new CookieCsrfTokenRepository();
csrfTokenRepository.setCookieCustomizer(builder->{
builder.domain("kongjs.com");
builder.path("/");
builder.httpOnly(false);
builder.sameSite("None");
builder.secure(true);
});
CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();
csrf
.ignoringRequestMatchers("/api/auth/csrf")
.csrfTokenRepository(csrfTokenRepository)
.csrfTokenRequestHandler(csrfTokenRequestAttributeHandler);
})
.formLogin(AbstractHttpConfigurer::disable)
/*.oneTimeTokenLogin(ott -> ott
.tokenGeneratingUrl("/api/auth/ott/send")
.loginProcessingUrl("/api/auth/ott/verify")
.showDefaultSubmitPage(false)
.successHandler(restAuthenticationSuccessHandler)
.failureHandler(restAuthenticationFailureHandler)
)*/
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/api/auth/**", "/error").permitAll()
.requestMatchers("/api/cms/**").permitAll()
.anyRequest().access(apiAuthorizationManager)
)
.oauth2ResourceServer(oauth2ResourceServer -> {
CommonBearerTokenResolver bearerTokenResolver = new CommonBearerTokenResolver();
bearerTokenResolver.setAllowCookieToken(true);
bearerTokenResolver.setAllowUriQueryParameter(true);
bearerTokenResolver.setAllowFormEncodedBodyParameter(true);
bearerTokenResolver.setIgnorePath("/api/auth");
oauth2ResourceServer
.accessDeniedHandler(restAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint)
.bearerTokenResolver(bearerTokenResolver)
.authenticationManagerResolver(authenticationManagerResolver);
})
.requestCache(RequestCacheConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.securityContext(ctx -> ctx.securityContextRepository(new RequestAttributeSecurityContextRepository()))
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(restAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint)
);
http.addFilterAfter(userFilter, AnonymousAuthenticationFilter.class);
return http.build();
}
}
JwtConfig 类描述
JwtConfig 为 OAuth2 / JWT 令牌基础设施配置类,proxyBeanMethods=false,提供 RSA 非对称密钥、JWK 密钥集、JWT 编解码器、不透明令牌内省器,为授权服务器与资源服务器提供令牌密码学能力。
核心职责
- 动态生成 RSA‑2048 密钥对 :程序启动时内存生成 RSA 2048 公私密钥对,用于 JWT 的签名验签;密钥驻留内存,服务重启密钥会重新生成,旧 JWT 全部失效。
- 构建 JWKSource :把 RSA 公私钥封装为
RSAKey,分配随机 kid (key id),包装成ImmutableJWKSet;对外提供 JWKS 密钥集合,供资源服务、客户端获取公钥做 JWT 验签。 - JwtDecoder:JWT 解码验签:基于 JWKSource 创建解码器,使用公钥校验 JWT 签名、解析 JWT 载荷,Spring Security 资源服务器依赖此 Bean 校验 JWT 令牌合法性。
- JwtEncoder:JWT 编码签发 :
NimbusJwtEncoder,使用 RSA 私钥对 JWT 进行签名,授权服务器颁发 JWT 令牌时使用。 - OpaqueTokenIntrospector 不透明令牌内省 :实例化
RestClientOpaqueTokenIntrospector,配置远程内省地址https://kongjs.com;用于处理不透明令牌(非 JWT 字符串令牌),资源服务 HTTP 调用授权服务器内省接口校验令牌有效性、获取用户权限信息。
Bean 说明
表格
| Bean | 作用 |
|---|---|
| jwkSource | 内存 RSA‑JWK 密钥集,包含公私钥、kid 标识 |
| jwtDecoder | JWT 解析验签,资源服务器使用 |
| jwtEncoder | JWT 签名签发,授权服务器使用 |
| introspector | 远程不透明令牌内省器,对接授权服务 token introspect 端点 |
关键特点与风险备注
-
密钥内存生成 :每次应用启动都会生成全新 RSA 密钥,重启后历史 JWT 全部无法通过验签,生产环境不建议内存临时密钥,应外置持久化密钥文件 / 密钥库。
-
同时支持两种令牌模式:
- JWT 模式:本地 JWK 完成签名验签,不需要远程调用;
- Opaque 不透明令牌模式:每次鉴权都 HTTP 请求
https://kongjs.com内省接口,存在网络开销。
-
内省器硬编码地址
https://kongjs.com,多环境需要外部配置化,不支持环境切换。 -
底层基于 nimbus‑jose‑jwt 库实现,Spring Authorization Server 标准实现。
-
没有配置
SpringOpaqueTokenIntrospector,当前采用 RestClient 远程 http 内省。
在整个体系中的位置
授权服务器通过
jwtEncoder签发 RSA 签名 JWT;资源服务器通过jwtDecoder本地校验 JWT;如果是不透明 token,则交给introspector远程调用授权服务校验;配合前面SecurityConfigOAuth2 资源服务器配置,完成令牌解析。
潜在问题:
- RSA 密钥启动动态生成,重启旧令牌失效;
- introspector 地址硬编码,生产要放到配置文件;
- RestClientOpaqueTokenIntrospector 未配置 clientId/clientSecret,此处代码不完整,运行会报错。
java
package com.kongjs.smo.common.security.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.RestClientOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration(proxyBeanMethods = false)
public class JwtConfig {
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public OpaqueTokenIntrospector introspector() {
return RestClientOpaqueTokenIntrospector.withIntrospectionUri("https://kongjs.com").build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
认证和授权事件
可以做审计日志
java
@Configuration(proxyBeanMethods = false)
public class EventConfig {
@Bean
public DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
@Bean
public AuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
return new SpringAuthorizationEventPublisher(applicationEventPublisher);
}
}
/// 以下参考
/*
* 认证成功 注意不一定是登录成功
* */
@EventListener
public void onSuccess(AuthenticationSuccessEvent event) {
long timestamp = event.getTimestamp();
Authentication authentication = event.getAuthentication();
log.info("user: {} 认证成功", authentication.getName());
//addLoginLog(timestamp, authentication, true, null);
}
// 登录成功
@EventListener
public void onSuccess(InteractiveAuthenticationSuccessEvent event) {
long timestamp = event.getTimestamp();
Authentication authentication = event.getAuthentication();
log.info("user: {} 登录成功", UserUtil.getUserId());
addLoginLog(timestamp, authentication, true, null);
}
/*
* 认证失败
* */
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent event) {
long timestamp = event.getTimestamp();
Authentication authentication = event.getAuthentication();
AuthenticationException ex = event.getException();
log.error("user: {} 认证失败: {}", UserUtil.getUserId(), ex.getMessage());
//addLoginLog(timestamp, authentication, false, ex);
}
/*
* 授权失败事件
* 注: 默认不发布 AuthorizationGrantedEvent
* */
@EventListener
public void onFailure(AuthorizationDeniedEvent<?> event) {
long timestamp = event.getTimestamp();
Supplier<Authentication> supplier = event.getAuthentication();
Authentication authentication = supplier.get();
log.error("user: {} 验权失败", authentication.getName());
Object principal = authentication.getPrincipal();
taskExecutor.execute(() -> {
});
}
json响应处理
java
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
private final ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (response.isCommitted()) {
return;
}
response.setCharacterEncoding("UTF-8");
if (request.getRequestURI().startsWith("/api")) {
converter.write(R.forbidden(accessDeniedException.getMessage()), MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
} else {
response.setStatus(403);
}
}
}
java
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if (response.isCommitted()) {
return;
}
response.setCharacterEncoding("UTF-8");
if (request.getRequestURI().startsWith("/api")) {
converter.write(R.unauthorized(authException.getMessage()), MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
} else {
response.setStatus(401);
}
}
}
java
@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
responseJson(response,R.error(exception.getMessage()));
}
private void responseJson(HttpServletResponse response, R<?> r) throws IOException {
converter.write(r, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
}
}
java
@Slf4j
@Component
@SuppressWarnings("all")
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
@Resource
private JwtProperties jwtProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
responseJson(response, result(authentication));
}
private void responseJson(HttpServletResponse response, R<?> r) throws IOException {
converter.write(r, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
}
private R<?> result(Authentication authentication) {
try {
String name = authentication.getName();
Object principal = authentication.getPrincipal();
Long userId = 0L;
LoginUser loginUser = null;
if (principal instanceof LoginUser user) {
userId = user.getId();
loginUser = user;
}
String secret = jwtProperties.getSecret();
Duration expire = jwtProperties.getExpire();
Instant now = Instant.now().plus(expire);
JWSSigner signer = new MACSigner(secret);
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
.subject(name)
.issuer(jwtProperties.getIssuer())
.audience(jwtProperties.getAudience())
.issueTime(Date.from(now))
.notBeforeTime(Date.from(now))
.expirationTime(Date.from(now))
.claim("uid", userId)
.build();
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(jwtProperties.getAlgorithm());
JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm);
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet);
signedJWT.sign(signer);
String token = signedJWT.serialize();
return R.success(new LoginResponse(token, loginUser));
} catch (KeyLengthException e) {
return R.error("密钥配置错误");
} catch (JOSEException e) {
return R.error("签名令牌异常");
} catch (Exception e) {
log.error("登录异常: {}", e.getMessage());
return R.error("登录失败");
}
}
}
自定义认证过滤器
java
package com.kongjs.smo.common.security.filter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
public class AuthFilter extends AbstractAuthenticationProcessingFilter {
public AuthFilter(AuthenticationManager authenticationManager) {
super(PathPatternRequestMatcher.pathPattern(HttpMethod.POST, "/api/auth/login"), authenticationManager);
}
}
/// 配套使用转换
@Getter
@Setter
public class RestAuthenticationConverter implements AuthenticationConverter {
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
public RestAuthenticationConverter() {
this(new WebAuthenticationDetailsSource());
}
public RestAuthenticationConverter(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
this.authenticationDetailsSource = authenticationDetailsSource;
}
private final ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
@Override
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
LoginRequest loginRequest;
try {
loginRequest = (LoginRequest) converter.read(LoginRequest.class, new ServletServerHttpRequest(request));
} catch (IOException e) {
return null;
}
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());
result.setDetails(this.authenticationDetailsSource.buildDetails(request));
return result;
}
}
java
@Resource
private RestAuthenticationFailureHandler authenticationFailureHandler;
@Resource
private RestAuthenticationSuccessHandler authenticationSuccessHandler;
@Bean
public AuthFilter authFilter(@Lazy AuthenticationManager authenticationManager) {
AuthFilter authFilter = new AuthFilter(authenticationManager);
authFilter.setAuthenticationConverter(new RestAuthenticationConverter());
authFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return authFilter;
}
@Bean
public FilterRegistrationBean<AuthFilter> authFilterRegistration(AuthFilter authFilter) {
FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>(authFilter);
registration.setEnabled(false);
return registration;
}
整合资源认证
AuthConfig 类描述
AuthConfig 认证核心配置类,proxyBeanMethods=false,负责密码编码器、账号密码认证管理器、多令牌格式动态认证管理器解析器 、方法安全表达式处理器,为系统同时支持 JWT 令牌与 Opaque 不透明令牌提供认证基础设施,配合前面 SecurityConfig、JwtConfig 组成整套 OAuth2 资源服务安全体系。
核心职责
-
密码编码配置 使用
Argon2Password4jPasswordEncoder,采用 Argon2 算法做密码哈希,相比 BCrypt 抗 GPU 破解能力更强,作为系统用户密码加密器。 -
账号密码 AuthenticationManager 构建
DaoAuthenticationProvider,绑定UserDetailsService+ Argon2 密码编码器,实现传统用户名 / 密码登录认证逻辑,返回ProviderManager。 -
**动态多模式令牌解析器
AuthenticationManagerResolver<HttpServletRequest>**根据请求头TOKEN‑FORMAT路由不同认证管理器:-
请求头
TOKEN‑FORMAT: opaque→ 使用 OpaqueTokenAuthenticationProvider,走远程令牌内省校验不透明令牌; -
无该 Header 或其他值 → 使用 JwtAuthenticationProvider,本地 JWT 解码器校验 JWT 令牌;
实现单套接口同时兼容 JWT、Opaque 两种令牌体系。
-
-
内部工厂方法 jwt () /opaque ()
jwt():创建 JWT 认证 Provider,装配JwtBearerTokenAuthenticationConverter完成 JWT 转 Authentication 认证对象;opaque():创建不透明令牌认证 Provider,依赖OpaqueTokenIntrospector做远程令牌校验。
-
自定义方法安全表达式处理器 注入自定义
PermissionEvaluator,扩展 Spring Security 方法权限表达式,支持@PreAuthorize("hasPermission(...)")自定义权限判断逻辑,对接业务自定义权限评估。
代码中存在一个被注释的备用
authenticationManager(),内部使用NullAuthenticationProvider,仅占位无实际认证能力,未启用。
Bean 清单
表格
| Bean | 说明 |
|---|---|
| passwordEncoder | Argon2 密码哈希编码器 |
| authenticationManager | 用户名密码登录的认证管理器 (Dao 模式) |
| authenticationManagerResolver | 请求级别动态选择 JWT/Opaque 认证管理器,供 SecurityConfig 的 oauth2ResourceServer 使用 |
| methodSecurityExpressionHandler | 方法安全表达式处理器,注入自定义 PermissionEvaluator,支撑hasPermission()表达式 |
整体调用链路
HTTP 请求进入 → CommonBearerTokenResolver提取 token → AuthenticationManagerResolver读取TOKEN‑FORMAT请求头 → 选择 Jwt/Opaque 对应的AuthenticationManager执行令牌校验 → 生成 Authentication 存入上下文 → 后续交由ApiAuthorizationManager做 URL 动态鉴权;业务层可使用@PreAuthorize + hasPermission()做方法鉴权。
java
package com.kongjs.smo.common.security.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.authentication.AuthenticationManagerBeanDefinitionParser;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.JwtBearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.util.StringUtils;
@Configuration(proxyBeanMethods = false)
public class AuthConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new Argon2Password4jPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(daoAuthenticationProvider);
}
//@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(new AuthenticationManagerBeanDefinitionParser.NullAuthenticationProvider());
}
@Bean
public AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver(JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) {
return (request) -> {
String tokenFormat = request.getHeader("TOKEN-FORMAT");
if (StringUtils.hasText(tokenFormat) && tokenFormat.equalsIgnoreCase("opaque")) {
return opaque(opaqueTokenIntrospector);
}
return jwt(jwtDecoder);
};
}
public AuthenticationManager jwt(JwtDecoder jwtDecoder) {
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder);
authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter());
return new ProviderManager(authenticationProvider);
}
public AuthenticationManager opaque(OpaqueTokenIntrospector introspectionClient) {
return new ProviderManager(new OpaqueTokenAuthenticationProvider(introspectionClient));
}
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(PermissionEvaluator permissionEvaluator) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
token解析补充cookie
支持cookie方便使用jwt做sso
java
package com.kongjs.smo.common.security.resolver;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Setter
public final class CommonBearerTokenResolver implements BearerTokenResolver {
private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token";
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE);
private boolean allowFormEncodedBodyParameter = false;
private boolean allowUriQueryParameter = false;
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
private boolean allowCookieToken = false;
private String accessTokenCookieName = "ACCESS_TOKEN";
private String ignorePath = "/api/auth";
@Override
public @Nullable String resolve(final HttpServletRequest request) {
String uri = request.getRequestURI();
if (StringUtils.hasLength(ignorePath) && uri.startsWith(ignorePath)) {
return null;
}
String s0 = resolveFromAuthorizationHeader(request);
if (StringUtils.hasText(s0)) {
return s0;
}
String s1 = resolveAccessTokenFromCookie(request);
if (StringUtils.hasText(s1)) {
return s1;
}
String s2 = resolveAccessTokenFromQueryString(request);
if (StringUtils.hasText(s2)) {
return s2;
}
String s3 = resolveAccessTokenFromBody(request);
if (StringUtils.hasText(s3)) {
return s3;
}
return null;
}
private static @Nullable String resolveToken(@Nullable String... accessTokens) {
if (accessTokens == null || accessTokens.length == 0) {
return null;
}
String accessToken = null;
for (String token : accessTokens) {
if (accessToken == null) {
accessToken = token;
} else if (token != null) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
}
if (accessToken != null && accessToken.isBlank()) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("The requested token parameter is an empty string");
throw new OAuth2AuthenticationException(error);
}
return accessToken;
}
private @Nullable String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
}
private @Nullable String resolveAccessTokenFromCookie(HttpServletRequest request) {
if (!this.allowCookieToken) {
return null;
}
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
String tokenValue = null;
for (Cookie cookie : cookies) {
if (this.accessTokenCookieName.equals(cookie.getName())) {
if (tokenValue != null) {
BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in cookie");
throw new OAuth2AuthenticationException(error);
}
tokenValue = cookie.getValue();
}
}
return tokenValue;
}
private @Nullable String resolveAccessTokenFromQueryString(HttpServletRequest request) {
if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) {
return null;
}
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
}
private @Nullable String resolveAccessTokenFromBody(HttpServletRequest request) {
if (!this.allowFormEncodedBodyParameter
|| !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
|| HttpMethod.GET.name().equals(request.getMethod())) {
return null;
}
String queryString = request.getQueryString();
if (queryString != null && queryString.contains(ACCESS_TOKEN_PARAMETER_NAME)) {
return null;
}
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
}
}
补充权限(权限字符串匹配,使用二级缓存动态权限)
java
package com.kongjs.smo.system.auth.perm;
import com.kongjs.smo.common.security.util.UserUtil;
import com.kongjs.smo.system.cache.PermCacheLoader;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.io.Serializable;
import java.util.Set;
/*
*
* https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#_using_a_custom_bean_in_spel
* */
@Slf4j
@Component("permissionEvaluator")
public class PermissionEvaluatorImpl implements PermissionEvaluator {
private static final AntPathMatcher ANT_MATCHER = new AntPathMatcher();
@Resource
private PermCacheLoader permCacheLoader;
public boolean hasPermission(String permission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) {
return false;
}
if (authentication.getName().equalsIgnoreCase("SUPERADMIN")) {
return true;
}
Set<String> roles = permCacheLoader.getRolesByUser(UserUtil.getUserId());
if (permission.startsWith("ROLE_")) {
if (roles.contains("ROLE_SUPERADMIN")) {
return true;
}
boolean contains = roles.contains(permission);
if (contains) {
return true;
}
}
Set<String> permissions = permCacheLoader.getPermissionsByUser(UserUtil.getUserId());
if (permissions.contains(permission)) {
return true;
}
for (String authority : permissions) {
if (ANT_MATCHER.match(authority, permission)) {
return true;
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, @Nullable Object targetDomainObject, Object permission) {
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
用户信息加载
java
package com.kongjs.smo.system.auth.service;
import com.kongjs.smo.common.security.dto.LoginUser;
import com.kongjs.smo.system.cache.PermCacheLoader;
import com.kongjs.smo.system.entity.User;
import com.kongjs.smo.system.service.UserService;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@SuppressWarnings("all")
public final class AuthUserDetailsService implements UserDetailsService {
@Resource
private UserService userService;
@Resource
private PermCacheLoader permCacheLoader;
@Override
public LoginUser loadUserByUsername(String username) throws UsernameNotFoundException {
try {
// 轻量查询:仅取 id 用于二级缓存 key
Long userId = userService.getOneAs(QueryWrapper.create()
.select(User::getId)
.eq(User::getUsername, username), Long.class);
if (userId == null) {
throw new UsernameNotFoundException("用户名" + username + "无效");
}
// 走用户二级缓存(id 做 key),缓存未命中由 UserLoginLoader 自动查库
LoginUser loginUser = permCacheLoader.getLoginUser(userId);
return loginUser;
} catch (UsernameNotFoundException e) {
throw e;
} catch (Exception e) {
// 内部未知异常不抛,防止攻击
log.error("登录异常: {}", e.getMessage());
throw new InternalAuthenticationServiceException("认证失败,系统出错");
}
}
/**
* 清除指定用户ID的登录缓存(推荐入口,无需查库解析用户名)。
*/
public void removeCacheByUserId(Long userId) {
try {
permCacheLoader.removeByUserId(userId);
} catch (Exception e) {
log.error("删除用户登录缓存异常, userId={}, msg={}", userId, e.getMessage());
}
}
}
java
package com.kongjs.smo.system.auth.service;
import com.kongjs.smo.common.security.util.UserUtil;
import com.kongjs.smo.system.entity.UserAddress;
import com.kongjs.smo.system.entity.UserProfile;
import com.kongjs.smo.system.service.UserAddressService;
import com.kongjs.smo.system.service.UserProfileService;
import jakarta.annotation.Resource;
import org.springframework.security.oauth2.core.oidc.DefaultAddressStandardClaim;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class OidcUserInfoService {
@Resource
private UserProfileService userProfileService;
@Resource
private UserAddressService userAddressService;
public OidcUserInfo loadUserByUsername(String username) {
UserProfile profile = userProfileService.getById(UserUtil.getUserId());
UserAddress address = userAddressService.getById(UserUtil.getUserId());
OidcUserInfo.Builder builder = OidcUserInfo.builder();
if (profile != null) {
if (StringUtils.hasText(profile.getFullName())) {
builder.name(profile.getFullName());
}
if (StringUtils.hasText(profile.getGivenName())) {
builder.givenName(profile.getGivenName());
}
if (StringUtils.hasText(profile.getFamilyName())) {
builder.familyName(profile.getFamilyName());
}
if (StringUtils.hasText(profile.getMiddleName())) {
builder.middleName(profile.getMiddleName());
}
if (StringUtils.hasText(profile.getNickName())) {
builder.nickname(profile.getNickName());
}
if (StringUtils.hasText(profile.getPreferredUsername())) {
builder.preferredUsername(profile.getPreferredUsername());
}
if (StringUtils.hasText(profile.getProfile())) {
builder.profile(profile.getProfile());
}
if (StringUtils.hasText(profile.getPicture())) {
builder.picture(profile.getPicture());
}
if (StringUtils.hasText(profile.getWebsite())) {
builder.website(profile.getWebsite());
}
if (StringUtils.hasText(profile.getEmail())) {
builder.email(profile.getEmail());
}
if (profile.getEmailVerified() != null) {
builder.emailVerified(profile.getEmailVerified());
}
if (StringUtils.hasText(profile.getGender())) {
builder.gender(profile.getGender());
}
if (StringUtils.hasText(profile.getBirthdate())) {
builder.birthdate(profile.getBirthdate());
}
if (StringUtils.hasText(profile.getZoneInfo())) {
builder.zoneinfo(profile.getZoneInfo());
}
if (StringUtils.hasText(profile.getLocale())) {
builder.locale(profile.getLocale());
}
if (StringUtils.hasText(profile.getPhoneNumber())) {
builder.phoneNumber(profile.getPhoneNumber());
}
if (profile.getPhoneNumberVerified() != null) {
builder.phoneNumberVerified(profile.getPhoneNumberVerified());
}
if (profile.getUpdateAt() != null) {
builder.updatedAt(profile.getUpdateAt().toString());
}
}
if (address != null) {
DefaultAddressStandardClaim.Builder ab = new DefaultAddressStandardClaim.Builder();
boolean hasAny = false;
if (StringUtils.hasText(address.getFormatted())) {
ab.formatted(address.getFormatted());
hasAny = true;
}
if (StringUtils.hasText(address.getStreetAddress())) {
ab.streetAddress(address.getStreetAddress());
hasAny = true;
}
if (StringUtils.hasText(address.getLocality())) {
ab.locality(address.getLocality());
hasAny = true;
}
if (StringUtils.hasText(address.getRegion())) {
ab.region(address.getRegion());
hasAny = true;
}
if (StringUtils.hasText(address.getPostalCode())) {
ab.postalCode(address.getPostalCode());
hasAny = true;
}
if (StringUtils.hasText(address.getCountry())) {
ab.country(address.getCountry());
hasAny = true;
}
if (StringUtils.hasText(address.getFormatted())) {
builder.address(address.getFormatted());
}
}
return builder.build();
}
}
补充
接口实现登录(简单版)
java
package com.kongjs.smo.system.auth.api;
import com.kongjs.smo.common.mybatis.flex.enums.EnableEnum;
import com.kongjs.smo.common.security.dto.LoginRequest;
import com.kongjs.smo.common.security.dto.LoginResponse;
import com.kongjs.smo.common.security.dto.LoginUser;
import com.kongjs.smo.common.web.annotation.Api;
import com.kongjs.smo.common.web.exception.BusinessException;
import com.kongjs.smo.common.web.result.R;
import com.kongjs.smo.system.cache.PermCacheLoader;
import com.kongjs.smo.system.entity.User;
import com.kongjs.smo.system.service.UserService;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.*;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.UUID;
@Api("认证")
@RestController
@RequestMapping("/auth")
public class AuthController {
private final Clock clock = Clock.systemUTC();
@Resource
private JwtEncoder jwtEncoder;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UserService userService;
@GetMapping("/csrf")
public R<CsrfToken> csrf(HttpServletRequest request, CsrfToken csrfToken) {
String remoteAddr = request.getRemoteAddr();
return R.success(csrfToken);
}
@PostMapping("/refresh")
public R<LoginResponse> refresh(HttpServletRequest request, HttpServletResponse response) {
String remoteAddr = request.getRemoteAddr();
return R.success();
}
@PostMapping("/login")
public R<LoginResponse> login(HttpServletRequest request, HttpServletResponse response, @RequestBody LoginRequest loginRequest) {
String remoteAddr = request.getRemoteAddr();
String tenant = loginRequest.tenant();
String username = loginRequest.username();
String password = loginRequest.password();
User user = userService.getOne(QueryWrapper.create().eq(User::getStatus, EnableEnum.ENABLE).eq(User::getUsername, username));
if (user==null|| passwordEncoder.matches(password,user.getPassword())){
throw new BusinessException(400,"用户或者密码错误");
}
/*UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());
Authentication authentication;
try {
authentication = authenticationManager.authenticate(unauthenticated);
} catch (Exception e) {
return R.unauthorized(e.getMessage());
}
String name = authentication.getName();
Object principal = authentication.getPrincipal();*/
Long userId = user.getId();
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(user,loginUser);
/*if (principal instanceof LoginUser user) {
userId = user.getId();
loginUser = user;
}*/
String issuer = "smo";
Instant issuedAt = this.clock.instant();
Instant expiresAt;
JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256;
expiresAt = issuedAt.plus(Duration.ofDays(7));
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder()
.issuer(issuer)
.subject(loginRequest.username())
.audience(Collections.singletonList("https://kongjs.com"))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.id(UUID.randomUUID().toString());
claimsBuilder.notBefore(issuedAt);
claimsBuilder.claim("uid", userId);
JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
JwsHeader jwsHeader = jwsHeaderBuilder.build();
JwtClaimsSet claims = claimsBuilder.build();
Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
String tokenValue = jwt.getTokenValue();
ResponseCookie responseCookie = ResponseCookie.from("ACCESS_TOKEN", tokenValue)
.domain("kongjs.com")
.path("/")
.maxAge(Duration.ofDays(7))
.httpOnly(true)
.secure(true)
.sameSite("None").build();
String cookieString = responseCookie.toString();
response.setHeader(HttpHeaders.SET_COOKIE, cookieString);
return R.success();
}
}
jwt过滤器简单版
java
package com.kongjs.smo.common.security.filter;
import com.kongjs.smo.common.security.black.BlackComponent;
import com.kongjs.smo.common.security.config.JwtProperties;
import com.kongjs.smo.common.security.util.TokenUtil;
import com.kongjs.smo.common.web.result.R;
import com.kongjs.smo.system.cache.PermCacheLoader;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.web.ProjectingJacksonHttpMessageConverter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.text.ParseException;
import java.util.Date;
@SuppressWarnings("all")
public class JwtFilter extends OncePerRequestFilter {
private final ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
@Resource
private JwtProperties jwtProperties;
@Resource
private BlackComponent blackComponent;
@Resource
private PermCacheLoader permCacheLoader;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = TokenUtil.extractToken(request);
if (StringUtils.hasText(token)) {
response.setCharacterEncoding("UTF-8");
try {
SignedJWT signedJWT = SignedJWT.parse(token);
JWSVerifier verifier = new MACVerifier(jwtProperties.getSecret());
boolean verify = signedJWT.verify(verifier);
if (verify) {
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
Date expirationTime = jwtClaimsSet.getExpirationTime();
if (expirationTime.before(new Date())) {
responseJson(response, R.unauthorized("令牌已经过期"));
return;
}
if (blackComponent.isBlackToken(token)) {
responseJson(response, R.forbidden("令牌已经拉黑"));
return;
}
String subject = jwtClaimsSet.getSubject();
UserDetails user = permCacheLoader.getLoginUser(jwtClaimsSet.getLongClaim("uid"));
setAuthentication(user);
} else {
responseJson(response, R.forbidden("验证令牌失败"));
return;
}
} catch (JOSEException e) {
responseJson(response, R.error("签名校验失败"));
return;
} catch (ParseException e) {
responseJson(response, R.error("令牌格式错误"));
return;
} catch (UsernameNotFoundException e) {
responseJson(response, R.error(e.getMessage()));
return;
} catch (Exception e) {
responseJson(response, R.error("系统发生错误"));
return;
}
}
filterChain.doFilter(request, response);
}
private void responseJson(HttpServletResponse response, R<?> r) throws IOException {
converter.write(r, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
}
private void setAuthentication(UserDetails user) {
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.authenticated(user, null, user.getAuthorities());
context.setAuthentication(auth);
securityContextHolderStrategy.setContext(context);
}
}
public FilterRegistrationBean jwtFilterRegistration(JwtFilter jwtFilter) { FilterRegistrationBean registration = new FilterRegistrationBean<>(jwtFilter); registration.setEnabled(false); return registration; }