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

    • 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
    • 最佳实践

OAuth2 Resource Server

定义与作用

OAuth2 Resource Server(资源服务器) 是 OAuth2 架构中负责托管和保护用户资源的服务端点。它不信任任何直接来自客户端的请求,只认可由授权服务器(Authorization Server)颁发的有效 Access Token。当客户端携带 Token 请求资源时,资源服务器必须独立验证该 Token 的签名、有效期、发行方(issuer)以及权限范围(scope),然后决定是否放行。

对比自建用户体系的痛点

痛点自建 Session 方案OAuth2 Resource Server 方案
分布式 Session 同步多实例部署需引入 Redis / Spring Session 共享 Session,增加运维复杂度无状态设计,Token 自身携带所有认证信息,任意实例均可独立校验
跨服务身份传递服务间调用需伪造 Session 或传递 Cookie,安全性差统一使用 JWT Token,任意服务均可通过公钥独立校验,无需信任中心
授权服务器耦合认证逻辑与资源服务逻辑混在一起,难以拆分和扩展认证交给授权服务器,资源服务器只关注"Token 是否有效 + 权限是否足够",职责单一
凭证过期策略全局 Session 过期难以精细化控制每个 JWT 自带 exp(过期时间)和 iat(签发时间),资源服务器可独立判断 Token 生命周期

Spring Security 5.x 通过 spring-boot-starter-oauth2-resource-server 模块提供开箱即用的 Resource Server 支持,核心组件包括 JwtDecoder(负责解码和校验 JWT)和 JwtAuthenticationConverter(负责将 JWT Claims 转换为 Spring Security 的 Authentication 对象)。


核心原理

Resource Server JWT 校验流程

issuer-uri 与 jwk-set-uri 的区别

配置项作用配置方式适用场景
issuer-uri指向授权服务器的 OpenID Connect 发现端点,Resource Server 自动从 /.well-known/openid-configuration 获取 jwks_uri 等元数据spring.security.oauth2.resourceserver.jwt.issuer-uri: https://auth.example.com授权服务器支持 OIDC 发现协议,希望自动获取配置
jwk-set-uri直接显式配置 JSON Web Key Set(JWKS)端点地址,Resource Server 不再执行自动发现spring.security.oauth2.resourceserver.jwt.jwk-set-uri: https://auth.example.com/.well-known/jwks.json授权服务器不支持 OIDC 发现,或需要绕过自动发现以缩短启动时间

关键机制:NimbusJwtDecoder 获取 JWKS 后会缓存公钥,并在 Token 的 kid(Key ID)头部指示下选择对应的公钥进行签名验证。如果授权服务器轮换公钥,Decoder 会自动刷新 JWKS 缓存。


完整示例

示例一:基于 issuer-uri 的自动发现配置

场景说明:团队使用 Auth0 作为授权服务器,Spring Boot 应用作为资源服务器提供 REST API。Auth0 支持标准 OpenID Connect 发现协议,因此使用 issuer-uri 让 Spring Security 自动获取所有 JWT 校验所需的端点。

操作前配置(application.yml):

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://dev-abc123.auth0.com/

# 调试日志(开发环境)
logging:
  level:
    org.springframework.security.oauth2: DEBUG

操作后配置(Spring Security 5.x 配置类):

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 无状态 API 通常禁用 CSRF
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeRequests(authz -> authz
                .antMatchers("/api/public/**").permitAll()
                .antMatchers(HttpMethod.GET, "/api/articles/**").hasAuthority("SCOPE_read:articles")
                .antMatchers(HttpMethod.POST, "/api/articles/**").hasAuthority("SCOPE_write:articles")
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // Auth0 默认将权限放在 "permissions" claim 中
        grantedAuthoritiesConverter.setAuthoritiesClaimName("permissions");
        grantedAuthoritiesConverter.setAuthorityPrefix("SCOPE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        converter.setPrincipalClaimName("sub"); // 使用 JWT 的 sub 作为用户名
        return converter;
    }
}

结果分析:

  1. 应用启动时,NimbusJwtDecoder 自动向 https://dev-abc123.auth0.com/.well-known/openid-configuration 发送请求
  2. 从返回的 JSON 中提取 jwks_uri(如 https://dev-abc123.auth0.com/.well-known/jwks.json)
  3. 进一步请求 JWKS 端点,获取当前有效的公钥集合并缓存到内存
  4. 客户端携带 Authorization: Bearer <JWT> 请求 /api/articles/123
  5. JwtDecoder 解析 JWT Header 中的 kid,从缓存的 JWKS 中找到匹配的公钥,验证 RSA 签名
  6. 校验 iss 是否等于 https://dev-abc123.auth0.com/,exp 是否未过期,aud 是否包含当前应用
  7. 通过 JwtAuthenticationConverter 将 permissions claim 映射为 SCOPE_read:articles 等权限
  8. AuthorizationFilter 比对权限,发现 GET /api/articles/123 需要 SCOPE_read:articles,匹配成功,放行请求

示例二:基于 jwk-set-uri 的显式配置(企业内网场景)

场景说明:某金融机构内部使用自建的 OAuth2 授权服务器,出于安全策略禁止应用访问外部自动发现端点,只允许白名单内的 JWKS 端点。需要显式配置 jwk-set-uri 并禁用自动发现。

操作前配置(application.yml):

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://internal-auth.bank.com/jwks/keys
          # 显式配置 issuer 用于校验(可选但推荐)
          issuer-uri: https://internal-auth.bank.com

# 配置连接池和超时(企业内网通常有防火墙限制)
server:
  netty:
    connection-timeout: 5000

操作后配置(Spring Security 5.x 配置类,含自定义 JwtDecoder 以配置缓存和超时):

@Configuration
@EnableWebSecurity
public class InternalResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeRequests(authz -> authz
                .antMatchers("/health", "/actuator/health").permitAll()
                .antMatchers("/api/v1/accounts/**").hasAuthority("SCOPE_accounts.read")
                .antMatchers("/api/v1/transfers/**").hasAuthority("SCOPE_transfers.write")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        // 使用 NimbusJwtDecoder 直接配置 Nimbus 连接参数
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
            "https://internal-auth.bank.com/jwks/keys"
        ).restOperations(restOperations()).build();

        // 配置 JWT 校验策略:issuer, audience, 时间校验
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(
            "https://internal-auth.bank.com"
        );
        OAuth2TokenValidator<Jwt> withAudience = AudienceValidator.of("account-service");
        OAuth2TokenValidator<Jwt> validator = DelegatingOAuth2TokenValidator
            .withDefaults(List.of(withIssuer, withAudience));

        jwtDecoder.setJwtValidator(validator);
        return jwtDecoder;
    }

    @Bean
    public RestOperations restOperations() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(Duration.ofSeconds(5));
        factory.setReadTimeout(Duration.ofSeconds(5));
        return new RestTemplate(factory);
    }
}

// 自定义 Audience 校验器
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    private final String audience;

    private AudienceValidator(String audience) {
        this.audience = audience;
    }

    public static AudienceValidator of(String audience) {
        return new AudienceValidator(audience);
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        List<String> audiences = token.getAudience();
        if (audiences != null && audiences.contains(audience)) {
            return OAuth2TokenValidatorResult.success();
        }
        return OAuth2TokenValidatorResult.failure(
            new OAuth2Error("invalid_token", "Audience mismatch", null)
        );
    }
}

结果分析:

  1. 应用启动时直接请求 https://internal-auth.bank.com/jwks/keys,不再访问 /.well-known/openid-configuration
  2. 启动速度快于 issuer-uri 方式(减少一次 HTTP 往返),且完全符合企业网络白名单策略
  3. 自定义 RestOperations 设置 5 秒超时,防止 JWKS 端点不可用时无限阻塞启动线程
  4. 自定义 AudienceValidator 确保 Token 的 aud claim 包含 account-service,防止其他业务的 Token 误访问账户服务
  5. 当授权服务器轮换签名密钥时,带有旧 kid 的 JWT 首次访问会触发 JWKS 缓存刷新,自动获取新公钥
  6. 若 Token 过期或签名无效,Resource Server 返回 401 Unauthorized,响应体中可能包含 WWW-Authenticate: Bearer error="invalid_token" 头

易错场景与面试考点

易错场景:issuer-uri 末尾缺少或多余斜杠导致启动失败

issuer-uri 在 Spring Security 5.x 中严格用于匹配 JWT 的 iss claim。配置时若末尾缺少 /,而授权服务器发放的 JWT 中 iss 包含尾部斜杠,会导致 JwtDecoder 在启动时验证失败。

错误配置:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com   # 缺少尾部 /

JWT 中的 iss claim:

{
  "iss": "https://auth.example.com/",
  "sub": "user123",
  "aud": ["my-app"],
  "exp": 1718000000
}

错误日志(启动时):

Caused by: java.lang.IllegalStateException: 
The Issuer "https://auth.example.com/" provided in the configuration 
did not match the issuer "https://auth.example.com" from the well-known endpoint

正确做法:配置 issuer-uri 时严格与授权服务器的 issuer 字段保持一致。如果不确定,优先使用 jwk-set-uri 直接配置,或先在浏览器中访问 /.well-known/openid-configuration 确认 issuer 字段的确切值。

面试考点:

  • 面试官常问:如果 Resource Server 启动时授权服务器的 JWKS 端点不可用,应用会怎样?
  • 答:Spring Security 5.x 中 NimbusJwtDecoder 在首次获取 JWKS 时若失败,启动过程会抛出异常导致应用无法启动。生产环境中建议:① 使用 jwk-set-uri 并在配置阶段配置重试机制;② 或实现自定义 JwtDecoder 延迟加载公钥;③ 通过 Spring Boot 的 fail-fast=false 与自定义 BeanPostProcessor 做降级处理。若应用已启动后 JWKS 端点短暂不可用,已缓存的公钥仍可用于校验 Token,直到缓存过期。

版本说明:本节基于 Spring Security 5.x 编写。5.x 中 WebSecurityConfigurerAdapter 是标准基类,antMatchers 用于路径匹配。若升级到 6.x,需改用 SecurityFilterChain Bean 和 requestMatchers。

上一页
OAuth2 Client
下一页
第三方登录配置