乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

JwtDecoder

定义与作用

JwtDecoder 是 Spring Security OAuth2 Resource Server 模块中的核心接口,负责将字符串形式的 JWT 解码为 Jwt 对象,并验证其签名的有效性。如果说 JWT 是"身份证",那么 JwtDecoder 就是"验钞机"——确保你收到的 Token 是真实签发、未被篡改且在有效期内的。

在 Spring Security 的过滤器链中,BearerTokenAuthenticationFilter 从请求头中提取 Token 后,交给 JwtAuthenticationProvider 处理,后者调用 JwtDecoder.decode() 完成解码与校验。如果签名验证失败或 Claims 校验不通过,将抛出 JwtException,最终返回 401 响应。

核心原理

接口定义

public interface JwtDecoder {
    Jwt decode(String token) throws JwtException;
}

NimbusJwtDecoder:默认实现

NimbusJwtDecoder 基于成熟的 Nimbus JOSE + JWT 库实现,支持两种签名验证模式:

模式适用场景配置方式
JWKS(JSON Web Key Set)生产环境,公钥动态轮换配置 jwkSetUri 指向授权服务器的 JWKS 端点
硬编码公钥开发测试、单密钥固定场景直接传入 RSAPublicKey 或 SecretKey

JWKS 验证签名流程

JWKS 端点(如 https://auth-server/.well-known/jwks.json)返回一组 JWK(JSON Web Key),每个 JWK 包含 kid(Key ID)。JWT 的 Header 中通常携带 kid,NimbusJwtDecoder 通过 kid 从缓存的 JWK 集合中匹配对应公钥进行验证。

内置 Claims 校验

NimbusJwtDecoder 自动校验以下标准 Claims:

Claim校验逻辑校验失败异常
exp(过期时间)exp < 当前时间 时 Token 已过期JwtExpiredTokenException
nbf(生效时间)nbf > 当前时间 时 Token 尚未生效JwtNotBeforeException
iss(签发者)与配置的 issuer-uri 比对(可选)JwtValidationException
aud(受众)与配置的受众比对(可选)JwtValidationException

解码与验证流程

示例一:通过 JWKS 端点配置 NimbusJwtDecoder

场景:企业使用独立的 OAuth2 授权服务器(如 Keycloak、Auth0),公钥可能定期轮换,资源服务器通过 JWKS 端点动态获取公钥。

操作前配置:

@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .antMatcher("/api/**")
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

此时 Spring Boot 自动配置会尝试从 application.yml 读取 spring.security.oauth2.resourceserver.jwt.issuer-uri 或 jwk-set-uri。若缺少配置,启动会报错。

操作后配置(显式声明 JwtDecoder):

@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    private String jwkSetUri;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .antMatcher("/api/**")
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                .decoder(jwtDecoder());
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    }
}

application.yml 配置:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://auth-server.example.com/.well-known/jwks.json

结果分析:

  • NimbusJwtDecoder.withJwkSetUri() 构建的解码器会缓存 JWKS 响应,默认 5 分钟刷新一次
  • JWT 的 Header 中携带 kid,解码器自动匹配对应公钥验证签名
  • 授权服务器轮换公钥时,资源服务器无需重启,下次刷新自动获取新密钥
  • 若请求携带的 JWT 被篡改,签名验证失败,抛出 BadJwtException,返回 401

示例二:使用本地 RSA 公钥配置 NimbusJwtDecoder

场景:开发环境或内部微服务间通信,使用固定 RSA 密钥对,资源服务器直接持有公钥验证。

操作前:项目未配置 JwtDecoder,启动时提示缺少 JWK 配置。

操作后配置:

@EnableWebSecurity
public class LocalKeyResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                .decoder(jwtDecoder());
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        // 从 classpath 加载 RSA 公钥
        RSAPublicKey publicKey = loadPublicKey();
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }

    private RSAPublicKey loadPublicKey() {
        try {
            Resource resource = new ClassPathResource("keys/public.pem");
            String key = new String(resource.getInputStream().readAllBytes())
                    .replace("-----BEGIN PUBLIC KEY-----", "")
                    .replace("-----END PUBLIC KEY-----", "")
                    .replaceAll("\\s", "");
            byte[] decoded = Base64.getDecoder().decode(key);
            X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return (RSAPublicKey) keyFactory.generatePublic(spec);
        } catch (Exception e) {
            throw new RuntimeException("无法加载 RSA 公钥", e);
        }
    }
}

结果分析:

  • NimbusJwtDecoder.withPublicKey() 使用本地 RSA 公钥验证签名,无需网络请求
  • 适用于授权服务器与资源服务器为同一团队维护的场景
  • 公钥轮换时需要重新部署资源服务器,维护成本高于 JWKS 模式
  • RS256 非对称算法下,授权服务器用私钥签名,资源服务器用公钥验证,私钥不外泄,安全可控

易错场景:时钟偏移导致 exp/nbf 验证失败

面试高频考点

问题现象:资源服务器部署在容器或云服务器上,JWT 明明未过期,却频繁收到 401,日志显示 JwtExpiredTokenException 或 JwtNotBeforeException。

根因分析:

  • NimbusJwtDecoder 默认使用系统时钟校验 exp 和 nbf
  • 如果资源服务器与授权服务器存在时钟偏差(如 NTP 未同步、时区不一致),可能出现:
    • 资源服务器时钟偏快:Token 实际未过期,但服务器认为已过期
    • 资源服务器时钟偏慢:Token 的 nbf 在未来,服务器认为 Token 尚未生效

错误排查(禁止用于生产):

部分开发者遇到时钟问题后,尝试完全关闭 exp 校验,这是严重的安全漏洞:

// 错误且危险:任何已过期的 Token 都将被接受
@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    // 不要移除或绕过时间校验
    return decoder;
}

正确做法:

  1. 优先同步 NTP:确保所有服务器时钟一致,这是根本解决方案
  2. 配置时钟容差:允许合理的时钟偏差(如 60 秒)
@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    
    // 标准时间校验,允许 60 秒时钟容差
    OAuth2TokenValidator<Jwt> withTimestamp = 
        new JwtTimestampValidator(Duration.ofSeconds(60));
    
    // 签发者校验(可选,根据 issuer-uri 配置)
    OAuth2TokenValidator<Jwt> withIssuer = new JwtIssuerValidator(issuerUri);
    
    // 组合验证器
    OAuth2TokenValidator<Jwt> validator = withTimestamp.and(withIssuer);
    
    decoder.setJwtValidator(validator);
    return decoder;
}

核心要点:JwtTimestampValidator 通过 Duration 参数配置时钟容差,生产环境建议设置为 30~60 秒,同时务必保障服务器 NTP 同步。绝对禁止为图省事而关闭时间校验。

上一页
JWT
下一页
JWT 认证