下载工作台
Spring Cloud 微服务

OAuth2 资源服务器

试读上半部分 · 解锁后可读全文

第 10 章 · OAuth2 资源服务器

本章目标:在微服务架构中实现 OAuth2 资源服务器(Resource Server);由虚构 auth.example.com 签发 JWT,gateway-svcuser-svc / order-svc 校验 Token;掌握 Bearer Token 透传scope/role 映射服务间调用身份;对比单体 ch15 自签 JWT 与中心化授权服务器;理解 Opaque Token + Introspection(概念)。

学时建议:5~6 小时(含 2 小时 auth-svc 与 Gateway 联调)

前置spring-cloud-web ch01~ch09spring-boot-web ch06 Security 与 ch15 JWT 概念。


10.1 微服务鉴权分层

┌──────────────┐    授权码/密码(教学简化)    ┌──────────────┐
│   SPA 客户端  │ ──────────────────────────► │  auth-svc    │
│              │ ◄────────────────────────── │  签发 JWT     │
└──────┬───────┘         access_token         └──────────────┘
       │ Authorization: Bearer <jwt>
       ▼
┌──────────────┐   校验 JWT / 透传 Header    ┌──────────────┐
│ gateway-svc  │ ──────────────────────────► │ order-svc    │
│  可选统一鉴权 │                             │ 资源服务器    │
└──────────────┘                             └──────────────┘
层级职责
授权服务器 auth-svc登录、签发/刷新 Token
Gateway可选统一 JWT 校验、黑名单、限流
各业务服务资源服务器,校验 scope、数据权限
服务间 Feign传递用户上下文或 Client Credentials(机器间)
虚构 auth.example.com严禁使用真实公司 IdP 密钥或生产 Client Secret。

10.2 授权服务器 auth-svc(教学简化)

使用 Spring Authorization Server(独立模块,与业务 user-svc 分离):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

注册 OAuth2 客户端(SPA,教学用 PKCE 可选):

@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient spa = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("svc-spa-client")
        .clientSecret("{noop}dev-secret-change-me")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 教学仅 dev
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .scope("openid")
        .scope("order:read")
        .scope("order:write")
        .tokenSettings(TokenSettings.builder()
            .accessTokenTimeToLive(Duration.ofHours(2))
            .build())
        .build();
    return new InMemoryRegisteredClientRepository(spa);
}
重要警告:密码模式(Resource Owner Password Credentials)在 OAuth 2.1 中已被移除,且 Spring Authorization Server 官方并未实现该模式——上面的注册示例仅用于理解 RegisteredClient 结构,直接启动并请求密码模式会得到 unsupported_grant_type。若要真正跑通密码模式需自行扩展 OAuth2TokenGrantAuthenticationConverter(不推荐)。生产环境请使用:用户场景 → 授权码 + PKCE;服务间 → Client Credentials(见 10.6)。

10.3 JWT 定制 Claims

@Component
public class CustomTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
    @Override
    public void customize(JwtEncodingContext context) {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            Authentication auth = context.getPrincipal();
            context.getClaims().claim("roles", extractRoles(auth));
            context.getClaims().claim("userId", extractUserId(auth));
        }
    }
}

JWT payload 示例:

{
  "sub": "alice",
  "userId": 7,
  "roles": ["USER"],
  "scope": "order:read order:write",
  "iss": "https://auth.example.com",
  "exp": 1735689600
}

10.4 业务服务:资源服务器配置

order-svc SecurityFilterChain

@Bean
@Order(2)
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/api/v1/orders/**").hasAuthority("SCOPE_order:read")
            .requestMatchers(HttpMethod.POST, "/api/v1/orders/**").hasAuthority("SCOPE_order:write")
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter())));
    return http.build();
}

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          jwk-set-uri: https://auth.example.com/oauth2/jwks

本地开发可用 http://127.0.0.1:9000 并关闭 HTTPS 校验(仅 dev profile)。


10.5 Gateway 统一鉴权(推荐)

gateway-svc 校验 JWT,下游信任内网并透传用户信息:

spring:
  cloud:
    gateway:
      routes:
        - id: route-order
          uri: lb://order-svc
          predicates:
            - Path=/api/v1/orders/**
          filters:
            - TokenRelay=   # 把 Authorization 头透传给下游(需引入 spring-boot-starter-oauth2-client)
注意:Gateway 过滤器参数里的 SpEL(如 #{@bean.apply(exchange)}无法访问 exchange 变量,AddRequestHeader 只适合写静态值。要把 JWT 中的 userId 动态注入请求头,必须用自定义 GlobalFilter
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {

    private final ReactiveJwtDecoder jwtDecoder;   // 由资源服务器自动配置注入

    public JwtAuthGlobalFilter(ReactiveJwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String auth = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (auth == null || !auth.startsWith("Bearer ")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。