SpringSercuirty整合jwt

SecurityConfig 类描述

SecurityConfig 是 Spring Security6 的 Web 安全核心配置类,标注 @EnableWebSecurity@EnableMethodSecurity,构建 SecurityFilterChain 安全过滤链,专门负责 /api/**/actuator/** 接口的安全防护,采用无状态 JWT/OAuth2 资源服务器模式 ,对接自定义动态权限管理器 ApiAuthorizationManager,前后端分离 RESTful 接口安全方案。

核心职责

  1. 安全匹配范围 :过滤链仅拦截 /api/**/actuator/** 请求,只处理后端 API 接口安全;
  2. HTTP 安全响应头加固:配置防嵌套 iframe、XSS 防护、Referrer 策略、权限策略等响应头,提升浏览器侧安全;
  3. CSRF 防护配置 :使用 Cookie 存储 CSRF Token,Cookie 配置域名、SameSite=None、Secure,支持前端 JS 读取 token;放行 /api/auth/csrf 接口跳过 CSRF 校验;
  4. 放行白名单接口 :静态资源、/api/auth/**/error/api/cms/** 直接放行无需鉴权;其余全部接口交给自定义 apiAuthorizationManager数据库动态权限鉴权
  5. OAuth2 资源服务器配置 :自定义 CommonBearerTokenResolver,多渠道解析 Token:Header、Cookie、URL 查询参数、表单参数均可携带 token;设置忽略 token 解析路径;注入自定义未认证、无权限异常处理器;使用外部 AuthenticationManagerResolver 做 token 认证解析;
  6. 会话与安全上下文STATELESS 无状态模式,不创建 session;安全上下文存储在 RequestAttribute,不依赖 session;禁用 requestCache,关闭登录跳转缓存;
  7. 登录表单关闭:禁用原生 formLogin 表单登录,完全使用 REST JSON 自定义处理器;
  8. 自定义过滤器植入 :将UserFilter放在AnonymousAuthenticationFilter之后执行,做用户预处理;
  9. 全局异常处理 :统一接入RestAuthenticationEntryPoint(未登录)、RestAccessDeniedHandler(权限不足),输出 JSON 格式错误,而非页面跳转;
  10. 开启方法级安全@EnableMethodSecurity(mode = AdviceMode.ASPECTJ),支持 @PreAuthorize 注解做方法粒度权限控制。

关键组件依赖

  • ApiAuthorizationManager:自定义动态 URL 权限管理器,数据库驱动接口鉴权;
  • UserFilter:自定义用户预处理过滤器;
  • CommonBearerTokenResolver:多来源 token 解析器;
  • RestxxxHandler:全套 REST 风格安全异常处理器,返回 JSON 响应;
  • AuthenticationManagerResolver:动态解析认证管理器,用于 token 校验。

技术特征

  1. proxyBeanMethods=false,减少 Spring 代理,提升启动性能;开启 webSecurity debug 调试;
  2. CSRF 采用 Cookie 模式适配前后端分离,前端 JS 读取 Cookie 里的 csrfToken 放入请求头;
  3. 完全无状态,不依赖服务端 session,适配 JWT 令牌体系;
  4. 两层鉴权:URL 层由ApiAuthorizationManager动态数据库权限;业务层可使用@PreAuthorize方法注解;
  5. oauth2 资源服务器不局限 Header 的 Authorization,支持 cookie、query、form 多种方式传 token,适配多种客户端场景。

备注潜在关注点

  1. CSRF Cookie 写死domain("kongjs.com"),部署多环境需要注意环境适配;Secure=true 要求必须 HTTPS;SameSite=None 配合跨域;
  2. @EnableMethodSecurity 使用 ASPECTJ 模式,需要编译期 / 加载期织入,否则方法注解不会生效;
  3. 过滤链 securityMatcher 限定只处理 /api/**,非该前缀接口不走此套安全规则;
  4. actuator 端点纳入此安全链管控,会受鉴权逻辑约束;
  5. 注释掉 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 编解码器、不透明令牌内省器,为授权服务器与资源服务器提供令牌密码学能力。

核心职责

  1. 动态生成 RSA‑2048 密钥对 :程序启动时内存生成 RSA 2048 公私密钥对,用于 JWT 的签名验签;密钥驻留内存,服务重启密钥会重新生成,旧 JWT 全部失效
  2. 构建 JWKSource :把 RSA 公私钥封装为 RSAKey,分配随机 kid (key id),包装成 ImmutableJWKSet;对外提供 JWKS 密钥集合,供资源服务、客户端获取公钥做 JWT 验签。
  3. JwtDecoder:JWT 解码验签:基于 JWKSource 创建解码器,使用公钥校验 JWT 签名、解析 JWT 载荷,Spring Security 资源服务器依赖此 Bean 校验 JWT 令牌合法性。
  4. JwtEncoder:JWT 编码签发NimbusJwtEncoder,使用 RSA 私钥对 JWT 进行签名,授权服务器颁发 JWT 令牌时使用。
  5. OpaqueTokenIntrospector 不透明令牌内省 :实例化 RestClientOpaqueTokenIntrospector,配置远程内省地址 https://kongjs.com;用于处理不透明令牌(非 JWT 字符串令牌),资源服务 HTTP 调用授权服务器内省接口校验令牌有效性、获取用户权限信息。

Bean 说明

表格

Bean 作用
jwkSource 内存 RSA‑JWK 密钥集,包含公私钥、kid 标识
jwtDecoder JWT 解析验签,资源服务器使用
jwtEncoder JWT 签名签发,授权服务器使用
introspector 远程不透明令牌内省器,对接授权服务 token introspect 端点

关键特点与风险备注

  1. 密钥内存生成 :每次应用启动都会生成全新 RSA 密钥,重启后历史 JWT 全部无法通过验签,生产环境不建议内存临时密钥,应外置持久化密钥文件 / 密钥库。

  2. 同时支持两种令牌模式:

    • JWT 模式:本地 JWK 完成签名验签,不需要远程调用;
    • Opaque 不透明令牌模式:每次鉴权都 HTTP 请求 https://kongjs.com 内省接口,存在网络开销。
  3. 内省器硬编码地址 https://kongjs.com,多环境需要外部配置化,不支持环境切换。

  4. 底层基于 nimbus‑jose‑jwt 库实现,Spring Authorization Server 标准实现。

  5. 没有配置SpringOpaqueTokenIntrospector,当前采用 RestClient 远程 http 内省。

在整个体系中的位置

授权服务器通过 jwtEncoder 签发 RSA 签名 JWT;资源服务器通过 jwtDecoder 本地校验 JWT;如果是不透明 token,则交给 introspector 远程调用授权服务校验;配合前面 SecurityConfig OAuth2 资源服务器配置,完成令牌解析。
潜在问题:

  1. RSA 密钥启动动态生成,重启旧令牌失效;
  2. introspector 地址硬编码,生产要放到配置文件;
  3. 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 不透明令牌提供认证基础设施,配合前面 SecurityConfigJwtConfig 组成整套 OAuth2 资源服务安全体系。

核心职责

  1. 密码编码配置 使用 Argon2Password4jPasswordEncoder,采用 Argon2 算法做密码哈希,相比 BCrypt 抗 GPU 破解能力更强,作为系统用户密码加密器。

  2. 账号密码 AuthenticationManager 构建 DaoAuthenticationProvider,绑定 UserDetailsService + Argon2 密码编码器,实现传统用户名 / 密码登录认证逻辑,返回 ProviderManager

  3. **动态多模式令牌解析器 AuthenticationManagerResolver<HttpServletRequest>**根据请求头 TOKEN‑FORMAT 路由不同认证管理器:

    • 请求头 TOKEN‑FORMAT: opaque → 使用 OpaqueTokenAuthenticationProvider,走远程令牌内省校验不透明令牌;

    • 无该 Header 或其他值 → 使用 JwtAuthenticationProvider,本地 JWT 解码器校验 JWT 令牌;

    实现单套接口同时兼容 JWT、Opaque 两种令牌体系。

  4. 内部工厂方法 jwt () /opaque ()

    • jwt():创建 JWT 认证 Provider,装配JwtBearerTokenAuthenticationConverter完成 JWT 转 Authentication 认证对象;
    • opaque():创建不透明令牌认证 Provider,依赖OpaqueTokenIntrospector做远程令牌校验。
  5. 自定义方法安全表达式处理器 注入自定义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; }

复制代码
相关推荐
岁月如歌77861 小时前
一次讲透 Redis 缓存击穿、穿透、雪崩,从原理到实战解决方案
java·后端·架构
IT_陈寒1 小时前
Vue的v-for为啥把我的渲染顺序搞乱套了?
前端·人工智能·后端
用户106911794101 小时前
SpringSecuirty自定义接口或者资源权限
后端
天涯明月19932 小时前
SGLang深度研究报告
人工智能·后端·推理·sglang
0xBADCODE2 小时前
Flask SSTI读SECRET_KEY+伪造Session:税务系统渗透全流程
前端·后端·python·安全·web安全·网络安全·flask
程序员爱钓鱼2 小时前
Rust where详解:让泛型与Trait约束更加清晰
前端·后端·rust
程序员爱钓鱼4 小时前
Go 编程实战:函数 Function——参数、返回值与代码复用
后端·面试·go
To_OC10 小时前
跑了3个Docker容器后,我终于搞懂镜像和容器到底啥关系
后端·docker·容器
东风破_10 小时前
Docker 基础:为什么需要容器?
前端·后端·nginx