19. OAuth2 WebFlux

Spring Security 为响应式应用程序提供了 OAuth2 和 WebFlux 集成。

19.1 OAuth 2.0 登录

OAuth 2.0 登录功能为应用程序提供了使用户能够通过使用他们在 OAuth 2.0 提供程序(例如 GitHub)或 OpenID Connect 1.0 提供程序(例如 Google)上的现有帐户登录到该应用程序的功能。 OAuth 2.0 登录实现了以下用例:“使用 Google 登录”或“使用 GitHub 登录”。

Note

OAuth 2.0 登录通过使用OAuth 2.0 授权框架OpenID Connect 核心 1.0中指定的 Authorization Code Grant 来实现。

19.1.1 Spring Boot 2.0 示例

Spring Boot 2.0 为 OAuth 2.0 登录带来了完整的自动配置功能。

本部分说明如何使用* Google 作为 Authentication Provider *配置OAuth 2.0 登录 WebFlux 示例,并涵盖以下主题:

Initial setup

要使用 Google 的 OAuth 2.0 身份验证系统进行登录,您必须在 Google API 控制台中设置一个项目以获得 OAuth 2.0 凭据。

Note

请按照“设置 OAuth 2.0”部分开始的OpenID Connect页上的说明进行操作。

完成“获取 OAuth 2.0 凭据”说明后,您应该拥有一个新的 OAuthClient 端,其凭据由 Client 端 ID 和 Client 端密钥组成。

设置重定向 URI

重定向 URI 是最终用户的用户代理在通过 Google 身份验证并授予“同意”页面上的 OAuthClient 端*(在上一步中创建)*的访问权限后,将重定向到该应用程序中的路径。

在“设置重定向 URI”子部分中,确保“授权重定向 URI”字段设置为http://localhost:8080/login/oauth2/code/google

Tip

默认重定向 URI 模板为{baseUrl}/login/oauth2/code/{registrationId}。 *** registrationId** *是ClientRegistration的唯一标识符。对于我们的示例,registrationIdgoogle

Configure application.yml

现在,您有了 Google 的新 OAuthClient 端,您需要配置应用程序以将 OAuthClient 端用于身份验证流程。为此:

  • 转到application.yml并设置以下配置:
spring:
  security:
    oauth2:
      client:
        registration:   (1)
          google:   (2)
            client-id: google-client-id
            client-secret: google-client-secret

例 19.1. OAuthClient 端属性

  • (1) spring.security.oauth2.client.registration是 OAuthClient 端属性的基本属性前缀。

  • (2) 基本属性前缀后面是ClientRegistration的 ID,例如 google。

  • client-idclient-secret属性中的值替换为您先前创建的 OAuth 2.0 凭据。

启动应用程序

启动 Spring Boot 2.0 示例并转到http://localhost:8080。然后,您将被重定向到默认的“自动生成”登录页面,该页面显示了 Google 的链接。

单击 Google 链接,然后您将重定向到 Google 进行身份验证。

在使用您的 Google 帐户凭据进行身份验证之后,显示给您的下一个页面是“同意”屏幕。 “同意”屏幕要求您允许或拒绝访问您之前创建的 OAuthClient 端。点击“允许”以授权 OAuthClient 端访问您的电子邮件地址和基本 Profile 信息。

此时,OAuthClient 端会从UserInfo Endpoint检索您的电子邮件地址和基本 Profile 信息,并构建经过身份验证的会话。

19.1.2 使用 OpenID 提供程序配置

对于知名的提供程序,Spring Security 为 OAuth 授权提供程序的配置提供了必要的默认值。如果您正在使用自己的支持OpenID 提供程序配置的授权提供程序,则可以使用OpenID 提供程序配置响应,可以使用 issuer-uri 来配置应用程序。

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: https://idp.example.com/auth/realms/demo
        registration:
          keycloak:
            client-id: spring-security
            client-secret: 6cea952f-10d0-4d00-ac79-cc865820dc2c

issuer-uri指示 Spring Security 利用https://idp.example.com/auth/realms/demo/.well-known/openid-configuration处的端点来发现配置。 client-idclient-secret链接到提供程序,因为keycloak用于提供程序和注册。

19.1.3 显式 OAuth2 登录配置

最小的 OAuth2 登录配置如下所示:

@Bean
ReactiveClientRegistrationRepository clientRegistrations() {
    ClientRegistration clientRegistration = ClientRegistrations
            .fromOidcIssuerLocation("https://idp.example.com/auth/realms/demo")
            .clientId("spring-security")
            .clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c")
            .build();
    return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .oauth2Login();
    return http.build();
}

其他配置选项如下所示:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .oauth2Login()
            .authenticationConverter(converter)
            .authenticationManager(manager)
            .authorizedClientRepository(authorizedClients)
            .clientRegistrationRepository(clientRegistrations);
    return http.build();
}

19.2 OAuth2Client 端

Spring Security 的 OAuth 支持允许无需身份验证即可获取访问令牌。 Spring Boot 的基本配置如下所示:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: replace-with-client-id
            client-secret: replace-with-client-secret
            scopes: read:user,public_repo

您将需要用在 GitHub 注册的值替换client-idclient-secret

下一步是指示 Spring Security 您希望充当 OAuth2Client 端,以便获得访问令牌。

@Bean
SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception {
    http
        // ...
        .oauth2Client();
    return http.build();
}

现在,您可以利用 Spring Security 的第 21 章,WebClient@RegisteredOAuth2AuthorizedClient支持来获取和使用访问令牌。

19.3 OAuth2 资源服务器

Spring Security 支持使用JWT编码的 OAuth 2.0 Bearer Tokens保护端点。

在应用程序将其权限 Management 联合到authorization server(例如 Okta 或 Ping Identity)的情况下,这很方便。资源服务器可以咨询该授权服务器,以在处理请求时验证权限。

Note

完整的工作示例可以在OAuth 2.0 资源服务器 WebFlux 示例中找到。

19.3.1 Dependencies

大多数资源服务器支持都收集在spring-security-oauth2-resource-server中。但是,对_JWT 进行解码和验证的支持在spring-security-oauth2-jose中,这意味着两者都必须具备,才能使工作的资源服务器支持 JWT 编码的承载令牌。

19.3.2 最低配置

使用Spring Boot时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。

指定授权服务器

在 Spring Boot 应用程序中,要指定要使用的授权服务器,只需执行以下操作:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com

其中https://idp.example.comiss索赔中包含的授权服务器将发出的 JWT 令牌的值。资源服务器将使用此属性进行进一步的自我配置,发现授权服务器的公钥,然后验证传入的 JWT。

Note

要使用issuer-uri属性,还必须确保https://idp.example.com/.well-known/openid-configuration是授权服务器支持的端点。该端点称为Provider Configuration端点。

就是这样!

Startup Expectations

使用此属性和这些依赖关系时,资源服务器将自动配置自身以验证 JWT 编码的承载令牌。

它通过确定性的启动过程来实现:

  • 点击提供者配置端点https://the.issuer.location/.well-known/openid-configuration,处理jwks_url属性的响应

  • 配置验证策略以查询jwks_url有效的公共密钥

  • 配置验证策略,以针对https://idp.example.com验证每个 JWT iss的声明。

此过程的结果是,授权服务器必须启动并接收请求,才能成功启动资源服务器。

Note

如果在资源服务器查询授权服务器时授权服务器已关闭(给出适当的超时),则启动将失败。

Runtime Expectations

应用程序启动后,资源服务器将尝试处理任何包含Authorization: BearerHeaders 的请求:

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

只要指示了此方案,资源服务器就会尝试根据 Bearer Token 规范处理请求。

给定格式正确的 JWT 令牌,资源服务器将:

  • 根据启动期间从jwks_url端点获取并与 JWT 头匹配的公钥来验证其签名

  • 验证 JWT expnbf的时间戳以及 JWT iss的声明,并

  • 将每个范围 Map 到具有前缀SCOPE_的授权。

Note

当授权服务器提供新的密钥时,Spring Security 将自动旋转用于验证 JWT 令牌的密钥。

默认情况下,结果Authentication#getPrincipal是 Spring Security Jwt对象,并且Authentication#getNameMap 到 JWT 的sub属性(如果存在)。

如何在不将资源服务器启动绑定到授权服务器的可用性的情况下进行配置

如何在没有 Spring Boot 的情况下进行配置

直接指定授权服务器 JWK 设置 Uri

如果授权服务器不支持提供者配置端点,或者资源服务器必须能够独立于授权服务器启动,则可以将issuer-uri交换为jwk-set-uri

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

Note

JWK Set uri 尚未标准化,但是通常可以在授权服务器的文档中找到

因此,资源服务器在启动时不会对授权服务器执行 ping 操作。但是,它也将不再验证 JWT 中的iss声明(因为 Resource Server 不再知道发行者的值应该是什么)。

Note

此属性也可以直接在DSL上提供。

覆盖或替换引导自动配置

Spring Boot 代表资源服务器生成两个@Bean

第一个是SecurityWebFilterChain,它将应用程序配置为资源服务器:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt();
    return http.build();
}

如果应用程序未公开SecurityWebFilterChain bean,那么 Spring Boot 将公开以上默认的SecurityWebFilterChain bean。

替换它就像在应用程序中公开 Bean 一样简单:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt();
    return http.build();
}

以上要求以/messages/开头的任何 URL 的范围为message:read

oauth2ResourceServer DSL 上的方法还将覆盖或替换自动配置。

例如,第二个@Bean Spring Boot 创建的是ReactiveJwtDecoder,它将String令牌解码为Jwt的经过验证的实例:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
}

如果应用程序未公开ReactiveJwtDecoder bean,那么 Spring Boot 将公开以上默认的ReactiveJwtDecoder bean。

可以使用jwkSetUri()覆盖其配置,也可以使用decoder()替换其配置。

Using jwkSetUri()

授权服务器的 JWK 设置 Uri 可以配置为作为配置属性,也可以在 DSL 中提供:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt()
                .jwkSetUri("https://idp.example.com/.well-known/jwks.json");
    return http.build();
}

使用jwkSetUri()优先于任何配置属性。

Using decoder()

jwkSetUri()更强大的是decoder(),它将完全取代JwtDecoder的所有 Boot 自动配置:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt()
                .decoder(myCustomDecoder());
    return http.build();
}

当需要更深的配置(如validation)时,这很方便。

公开 ReactiveJwtDecoder @Bean

或者,暴露ReactiveJwtDecoder @Beandecoder()具有相同的效果:

@Bean
public JwtDecoder jwtDecoder() {
    return new NimbusReactiveJwtDecoder(jwkSetUri);
}

Configuring Authorization

从 OAuth 2.0 授权服务器发出的 JWT 通常具有scopescp属性,指示已被授予的作用域(或权限),例如:

{ …, "scope" : "messages contacts"}

在这种情况下,资源服务器将尝试将这些作用域强制为已授予权限的列表,并为每个作用域添加字符串“ SCOPE_”作为前缀。

这意味着为了保护具有从 JWT 派生的作用域的端点或方法,相应的表达式应包含以下前缀:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
            .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt();
    return http.build();
}

或类似地具有方法安全性:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
手动提取权限

但是,在许多情况下,此默认设置不足。例如,某些授权服务器不使用scope属性,而是使用自己的自定义属性。或者,在其他时间,资源服务器可能需要将属性或属性组成调整为内部化的权限。

为此,DSL 公开了jwtAuthenticationConverter()

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt()
                .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
    return http.build();
}

Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
    GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor();
    return new ReactiveJwtAuthenticationConverterAdapter(extractor);
}

负责将Jwt转换为Authentication

我们可以很简单地覆盖此方法,以更改授予权限的方式:

static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {
    protected Collection<GrantedAuthorities> extractAuthorities(Jwt jwt) {
        Collection<String> authorities = (Collection<String>)
                jwt.getClaims().get("mycustomclaim");

        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

为了获得更大的灵 Active,DSL 支持使用实现Converter<Jwt, Mono<AbstractAuthenticationToken>>的任何类完全替代转换器:

static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return Mono.just(jwt).map(this::doConversion);
    }
}

Configuring Validation

使用指示授权服务器的发行者 uri,资源服务器将默认验证iss声明以及expnbf时间戳声明。

在需要自定义验证的情况下,资源服务器附带两个标准验证器,并且还接受自定义的OAuth2TokenValidator实例。

自定义时间戳验证

JWT 通常具有有效期窗口,该窗口的开始在nbf声明中指示,而结束在exp声明中指示。

但是,每台服务器都会经历时钟漂移,这可能导致令牌在一个服务器上显得过期,而在另一台服务器上过期。随着分布式系统中协作服务器数量的增加,这可能会导致某些实现上的胃口。

资源服务器使用JwtTimestampValidator来验证令牌的有效性窗口,并且可以将其配置为clockSkew来缓解上述问题:

@Bean
ReactiveJwtDecoder jwtDecoder() {
     NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
             ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri);

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new IssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}

Note

默认情况下,资源服务器将时钟偏差配置为 30 秒。

配置自定义验证器

使用OAuth2TokenValidator API 可以为aud声明添加支票很简单:

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

然后,要添加到资源服务器中,只需指定ReactiveJwtDecoder实例即可:

@Bean
ReactiveJwtDecoder jwtDecoder() {
    NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
            ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}