一次完整的 Spring Security JWT 鉴权链路解析

一次完整的 Spring Security JWT 鉴权链路解析

在实际项目中,我们经常会在 Controller 里写出这样一个方法签名:

java 复制代码
@GetMapping("/me")
public AuthUserResponse me(@AuthenticationPrincipal Jwt jwt) {
    long userId = jwtService.extractUserId(jwt);
    return authService.me(userId);
}

看起来 Spring 能"凭空"把一个 Jwt 对象塞进方法参数里,还自动从请求头里的 Authorization: Bearer 提取 Token 并完成校验。


这篇文章就结合项目,从配置代码,串起这条鉴权链路的每一步。


一、整体流程总览

从前端调用 /api/v1/auth/me 开始,到方法参数里拿到 Jwt jwt,整个链路可以概括为:

  1. 前端发请求 :HTTP 请求头里带 Authorization: Bearer <accessToken>
  2. Security 过滤器拦截SecurityFilterChain 中的 Bearer Token 过滤器从请求头里截出 <accessToken>
  3. 交给 JwtDecoder 校验解析 :过滤器调用配置好的 JwtDecoder(基于 RSA 公钥的 NimbusJwtDecoder),做签名、过期等校验,解析出 Claim,得到一个 Jwt 对象。
  4. 封装 Authentication 放入 SecurityContext :框架把 Jwt 封装成 JwtAuthenticationToken,写入当前线程的 SecurityContext
  5. Controller 使用 @AuthenticationPrincipal Jwt 注入当前用户 Token@AuthenticationPrincipalSecurityContext 里取出 principal(类型为 Jwt)注入到方法参数。
  6. 业务层解析用户 IDjwtService.extractUserId(jwt) 从 Claim 中读取 uid,实现当前登录用户识别与后续业务处理。

下面按照"配置层 → 过滤器层 → 控制器层"的顺序,一步步展开。


二、开启资源服务器模式:SecurityFilterChain 的核心配置

项目中 Spring Security 的入口配置在 SecurityConfig 中:

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(Customizer.withDefaults())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                    // 公开内容:首页 Feed 不需要登录
                    .requestMatchers("/api/v1/knowposts/feed").permitAll()
                    // 知文详情等其他白名单接口...
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
    return http.build();
}

这里有几个关键点:

  • 无状态会话SessionCreationPolicy.STATELESS
    • 服务端不使用 Session 记住"谁登录过",每个请求都必须自带 Token。
  • 访问控制
    • 白名单接口使用 permitAll() 直接放行;
    • 其他接口通过 .anyRequest().authenticated() 强制要求认证。
  • 启用 JWT 资源服务器模式oauth2ResourceServer(oauth -> oauth.jwt())
    • 告诉 Spring Security 本应用是一个 OAuth2 Resource Server
    • 请求需要通过 Bearer Token + JWT 的方式来进行认证;
    • 框架自动往过滤器链里加上 Bearer Token 相关的过滤器和认证器。

这样一来,我们就不需要自己在每个接口里手动解析请求头,整个 Token 解析与校验流程都由 Spring Security 统一接管。


三、JWT 编解码 Bean:JwtEncoderJwtDecoder

要让资源服务器真正"识别"JWT,我们必须告诉它如何签发和校验 Token,这部分逻辑集中在 AuthConfiguration 中:

java 复制代码
@Configuration
@EnableConfigurationProperties(AuthProperties.class)
@RequiredArgsConstructor
public class AuthConfiguration {

    private final AuthProperties properties;

    @Bean
    public JwtEncoder jwtEncoder() {
        AuthProperties.Jwt jwtProps = properties.getJwt();
        RSAPrivateKey privateKey = PemUtils.readPrivateKey(jwtProps.getPrivateKey());
        RSAPublicKey publicKey = PemUtils.readPublicKey(jwtProps.getPublicKey());

        RSAKey jwk = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(jwtProps.getKeyId())
                .build();
        JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        AuthProperties.Jwt jwtProps = properties.getJwt();
        RSAPublicKey publicKey = PemUtils.readPublicKey(jwtProps.getPublicKey());
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }
}
  • JwtEncoder

    • 读取配置中的 RSA 私钥、公钥与 keyId;
    • 构造基于 Nimbus 的 JwtEncoder
    • 用于登录/注册成功后签发 Access Token 与 Refresh Token。
  • JwtDecoder

    • 只读取 RSA 公钥;
    • 创建 NimbusJwtDecoder,用于校验签名、过期时间等信息并解析 Claim。

关键点

一旦容器中存在一个 JwtDecoder Bean,oauth2ResourceServer().jwt() 会自动使用它来完成 JWT 的解析与验证,无需手动再把 JwtDecoder 绑定到过滤器上。


四、自定义 JWT 服务:签发与解析的业务封装 JwtService

在业务层,我们通过 JwtService 把 Token 的签发与解析从 Controller 和 Service 中抽离出来:

java 复制代码
@Service
@RequiredArgsConstructor
public class JwtService {

    private static final String CLAIM_TOKEN_TYPE = "token_type";
    private static final String CLAIM_USER_ID = "uid";

    private final JwtEncoder jwtEncoder;
    private final JwtDecoder jwtDecoder;
    private final AuthProperties properties;
    private final Clock clock = Clock.systemUTC();

    public TokenPair issueTokenPair(User user) {
        String refreshTokenId = UUID.randomUUID().toString();
        Instant issuedAt = Instant.now(clock);
        Instant accessExpiresAt = issuedAt.plus(properties.getJwt().getAccessTokenTtl());
        Instant refreshExpiresAt = issuedAt.plus(properties.getJwt().getRefreshTokenTtl());
        String accessToken = encodeToken(user, issuedAt, accessExpiresAt, "access", UUID.randomUUID().toString());
        String refreshToken = encodeRefreshToken(user, issuedAt, refreshExpiresAt, refreshTokenId);
        return new TokenPair(accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, refreshTokenId);
    }

    public Jwt decode(String token) {
        return jwtDecoder.decode(token);
    }

    public long extractUserId(Jwt jwt) {
        Object claim = jwt.getClaims().get(CLAIM_USER_ID);
        if (claim instanceof Number number) {
            return number.longValue();
        }
        if (claim instanceof String text) {
            return Long.parseLong(text);
        }
        throw new IllegalArgumentException("Invalid user id in token");
    }
}

在这里:

  • 签发阶段

    • 使用 JwtEncoder 构造带有 uidtoken_typejti 等 Claim 的 Token;
    • Access Token 与 Refresh Token 使用不同的 TTL 与类型标记。
  • 解析阶段

    • 可以通过 decode() 手动解析某个 Token(例如刷新或登出时用);
    • 对于 /me 等需要"当前用户"的接口,更多使用 extractUserId(jwt),从已经被框架解析过的 Jwt 中读取 uid Claim。

五、请求进入时:Filter 如何一步步完成 JWT 鉴权

当一个请求携带:

  • GET /api/v1/auth/me
  • Authorization: Bearer <accessToken>

到达服务端时,Spring Security 会按如下步骤进行处理:

  1. 进入 SecurityFilterChain

    • 请求首先会进入配置好的安全过滤器链;
    • 因为启用了 .oauth2ResourceServer().jwt(),链中包含 Bearer Token 相关过滤器。
  2. 从请求头中提取 Bearer Token

    • 过滤器内部使用 BearerTokenResolver(默认实现为 DefaultBearerTokenResolver):
      • 从请求头里读取 Authorization
      • 匹配 Bearer 前缀;
      • 截取出 <accessToken> 部分。
  3. 创建未认证的 Authentication 并委托给 AuthenticationManager

    • 根据 Token 创建一个 BearerTokenAuthenticationToken,此时它还处于"未认证"状态;
    • 将该对象交由 AuthenticationManager(内部委托给 JwtAuthenticationProvider)处理。
  4. JwtAuthenticationProvider 使用 JwtDecoder 进行校验与解析

    • JwtAuthenticationProvider 注入的就是我们前面定义的 JwtDecoder Bean;
    • 调用 jwtDecoder.decode(accessToken)
      • 使用 RSA 公钥校验签名;
      • 校验 Token 是否过期、是否生效(exp/nbf 等);
      • 解析出完整的 Claim 集合并构造 Jwt 对象。
  5. 构造认证后的 JwtAuthenticationToken,写入 SecurityContext

    • 若校验通过,JwtAuthenticationProvider 会创建一个认证成功的 JwtAuthenticationToken
      • principal:解析得到的 Jwt
      • authorities:可根据 Claim 映射出的权限列表(本项目中暂未做复杂映射)。
    • 将这个 Authentication 写入当前线程的 SecurityContextHolder 中,代表"当前请求已认证"。
  6. 校验失败时的处理

    • 若没有 Token、Token 非法或过期,JwtDecoder 会抛出异常;
    • 资源服务器模块会返回 401 或 403 响应,Controller 不会被执行。

六、Controller 层:@AuthenticationPrincipal Jwt 的注入机制

回到 AuthController 中的 /me 接口:

java 复制代码
@GetMapping("/me")
public AuthUserResponse me(@AuthenticationPrincipal Jwt jwt) {
    long userId = jwtService.extractUserId(jwt);
    return authService.me(userId);
}

这里的 jwt 参数是如何被自动注入的?

  1. Spring MVC 参数解析 + Spring Security 协作

    • Spring MVC 调用 Handler 方法前,会为每个参数寻找"数据来源";
    • 当发现参数上有 @AuthenticationPrincipal 注解时,会启用专门的参数解析器:
      • SecurityContextHolder.getContext().getAuthentication() 中获取当前 Authentication
      • 默认取 authentication.getPrincipal()
      • 尝试匹配/转换为方法参数所声明的类型。
  2. 在资源服务器 JWT 场景下,principal 正是 Jwt

    • 前面的认证流程中,JwtAuthenticationProvider 构造的是 JwtAuthenticationToken
    • getPrincipal() 返回的就是 org.springframework.security.oauth2.jwt.Jwt 对象;
    • 因此,当方法参数声明为 @AuthenticationPrincipal Jwt jwt 时,类型就刚好匹配,可以直接注入。
  3. 业务层通过 Claim 完成"当前用户"的识别

    • 本项目在签发 Token 时把用户 ID 放在 uid Claim 中;
    • 因此在 /me 中,可以通过 jwtService.extractUserId(jwt) 从 Claim 集合中读取 uid,作为当前登录用户的唯一标识;
    • 后续调用 authService.me(userId) 查询用户信息并返回。

七、口头表达总结:如何在面试中讲清这条链路

如果在面试中被问到"你这个项目里 JWT 是如何鉴权的?@AuthenticationPrincipal Jwt 的 Jwt 从哪来的?",可以这样组织回答:

我这边是基于 Spring Security 的 OAuth2 Resource Server,做了基于 JWT 的无状态鉴权。不用 Session 记录登录状态,靠请求头中携带的 token 进行鉴权、记录登录状态。配置层面,配置基于 RSA 公钥的 JWT 的解码器注入 ioc 容器中。在 SpringSecurity 配置类中启用资源服务器的 JWT 模式,安全过滤器链中会添加一个 Bearer Token 过滤器,所有非白名单接口都会先走一遍这个过滤器,从请求头里提取 Token ,交由解码器进行校验与解析,若没有 Token、Token 非法或过期,解码器会抛出异常,返回 401 或 403 响应,Controller 不会被执行。校验通过后会构造一个 Jwt 对象交由线程上下文进行管理。Controller 里使用 @AuthenticationPrincipal Jwt jwt 这种方式,Spring MVC 会从 SecurityContext 中拿出这个 Jwt 对象注入到参数里。到这一步鉴权完成,然后通过我封装的方法,可以从 Jwt 对象的声明中(Claim)里取出登录的用户 ID,整个过程是完全无状态的。


八、总结

整条从 Authorization: Bearer@AuthenticationPrincipal Jwt 的链路,可以概括为四层:

  • 配置层SecurityFilterChain 启用 JWT Resource Server 模式,AuthConfiguration 提供 JwtEncoder/JwtDecoder Bean。
  • 过滤器层 :Bearer Token 过滤器从请求头里提取 Token,交由 JwtAuthenticationProvider 使用 JwtDecoder 校验与解析。
  • 安全上下文层 :认证通过后,JwtAuthenticationToken 被写入 SecurityContext,代表当前线程的认证状态。
  • 控制器层@AuthenticationPrincipal JwtSecurityContext 中获取 principal 注入到方法参数,业务通过 Claim 完成用户识别。

掌握这条链路之后,我们不仅能在项目中更好地调试和扩展安全逻辑,也能在面试中把"Spring Security + JWT 鉴权"讲得更清晰、更体系化。

相关推荐
蒹葭玉树2 小时前
【C++上岸】C++常见面试题目--操作系统篇(第二十九期)
java·c++·面试
小小仙。2 小时前
IT自学第二十天
java·开发语言
heartbeat..2 小时前
深入理解 JVM:从核心原理到实战应用
java·jvm·jdk·学习笔记
独自破碎E2 小时前
数组列表中的最大距离
java
猿小羽2 小时前
基于 Spring AI 与 Streamable HTTP 构建 MCP Server 实践
java·llm·spring ai·mcp·streamable http
大模型微调Online2 小时前
深度复盘:Qwen3-4B-Instruct-2507微调实战——打造“快思考、强执行”的 ReAct IoT Agent
java·后端·struts
铁蛋AI编程实战2 小时前
Agentic AI/GPT-4o替代/Spring AI 2.0/国产大模型轻量化
java·人工智能·spring
weixin_704266052 小时前
Maven入门:构建与依赖管理全解析
java·maven
cyforkk2 小时前
14、Java 基础硬核复习:数据结构与集合源码的核心逻辑与面试考点
java·数据结构·面试