OAuth 2.0 Resource Server Opaque Token

Minimal Dependencies for Introspection

Minimal Dependencies for JWT 中所述,大多数资源服务器支持都收集在 spring-security-oauth2-resource-server 中。但是,除非提供了自定义的 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>>,否则资源服务器将回退到 NimbusOpaqueTokenIntrospector。这意味着 spring-security-oauth2-resource-serveroauth2-oidc-sdk 都是必需的,才能获得支持不透明持有者令牌的极简资源服务器。请参阅 spring-security-oauth2-resource-server 以确定 oauth2-oidc-sdk 的正确版本。

As described in Minimal Dependencies for JWT most of Resource Server support is collected in spring-security-oauth2-resource-server. However unless a custom <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> is provided, the Resource Server will fallback to NimbusOpaqueTokenIntrospector. Meaning that both spring-security-oauth2-resource-server and oauth2-oidc-sdk are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens. Please refer to spring-security-oauth2-resource-server in order to determine the correct version for oauth2-oidc-sdk.

Minimal Configuration for Introspection

通常,可以通过由授权服务器托管的 OAuth 2.0 Introspection Endpoint 来验证不透明令牌。当撤销是必需条件时,这可能会很方便。

Typically, an opaque token can be verified via an OAuth 2.0 Introspection Endpoint, hosted by the authorization server. This can be handy when revocation is a requirement.

当使用 Spring Boot 时,将应用程序配置为使用自省的资源服务器包括两个基本步骤。首先,包含所需的依赖项,其次,指示自省端点详细信息。

When using Spring Boot, configuring an application as a resource server that uses introspection consists of two basic steps. First, include the needed dependencies and second, indicate the introspection endpoint details.

Specifying the Authorization Server

若要指定省察端点的位置,只需执行以下操作:

To specify where the introspection endpoint is, simply do:

spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://idp.example.com/introspect
          client-id: client
          client-secret: secret

其中 https://idp.example.com/introspect 是授权服务器托管的省察端点,client-idclient-secret 是访问该端点所需的凭据。

Where https://idp.example.com/introspect is the introspection endpoint hosted by your authorization server and client-id and client-secret are the credentials needed to hit that endpoint.

资源服务器将使用这些属性来进一步进行配置,然后验证传入的 JWT。

Resource Server will use these properties to further self-configure and subsequently validate incoming JWTs.

在使用自省时,授权服务器的话语就是法律。如果授权服务器响应令牌有效,则令牌就是有效的。

When using introspection, the authorization server’s word is the law. If the authorization server responses that the token is valid, then it is.

这就是全部!

And that’s it!

Startup Expectations

当使用此属性和这些依赖项时,资源服务器将自动配置自身以验证不透明持有者令牌。

When this property and these dependencies are used, Resource Server will automatically configure itself to validate Opaque Bearer Tokens.

此启动过程比 JWTs 要简单得多,因为无需发现端点,也不会添加其他验证规则。

This startup process is quite a bit simpler than for JWTs since no endpoints need to be discovered and no additional validation rules get added.

Runtime Expectations

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

Once the application is started up, Resource Server will attempt to process any request containing an Authorization: Bearer header:

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

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

So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.

给定一个不透明令牌,资源服务器将

Given an Opaque Token, Resource Server will

  1. Query the provided introspection endpoint using the provided credentials and the token

  2. Inspect the response for an { 'active' : true } attribute

  3. Map each scope to an authority with the prefix SCOPE_

默认情况下,生成的 Authentication#getPrincipal 是一个 Spring Security {security-api-url}org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html[OAuth2AuthenticatedPrincipal] 对象,如果存在令牌的 sub 属性,那么 Authentication#getName 会映射到该属性。

The resulting Authentication#getPrincipal, by default, is a Spring Security {security-api-url}org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html[OAuth2AuthenticatedPrincipal] object, and Authentication#getName maps to the token’s sub property, if one is present.

从这里,您可能希望跳转到:

From here, you may want to jump to:

How Opaque Token Authentication Works

接下来,让我们看看 Spring Security 用于支持基于 servlet 的应用程序(比如我们刚才看到的应用程序)中的 opaque token 身份验证的架构组件。

Next, let’s see the architectural components that Spring Security uses to support opaque token Authentication in servlet-based applications, like the one we just saw.

{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[OpaqueTokenAuthenticationProvider] 是 AuthenticationProvider 实现,它利用 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> 对不透明令牌进行身份验证。

{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[OpaqueTokenAuthenticationProvider] is an AuthenticationProvider implementation that leverages a <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> to authenticate an opaque token.

我们来看看 OpaqueTokenAuthenticationProvider 在 Spring Security 中如何工作的。此图解释了来自 Reading the Bearer Token 的图中的 AuthenticationManager 如何工作的详细信息。

Let’s take a look at how OpaqueTokenAuthenticationProvider works within Spring Security. The figure explains details of how the AuthenticationManager in figures from oauth2resourceserver-authentication-bearertokenauthenticationfilter works.

opaquetokenauthenticationprovider
Figure 1. OpaqueTokenAuthenticationProvider Usage

number 1 来自 Reading the Bearer Token 的身份验证 FilterAuthenticationManager 传递了 BearerTokenAuthenticationToken,而 ProviderManager 实现了 AuthenticationManager

number 1 The authentication Filter from oauth2resourceserver-authentication-bearertokenauthenticationfilter passes a BearerTokenAuthenticationToken to the AuthenticationManager which is implemented by ProviderManager.

number 2 ProviderManager 配置为使用类型为 OpaqueTokenAuthenticationProviderAuthenticationProvider

number 2 The ProviderManager is configured to use an AuthenticationProvider of type OpaqueTokenAuthenticationProvider.

number 3 OpaqueTokenAuthenticationProvider 通过 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> 内省不透明令牌并添加授予的权限。身份验证成功后,返回的 Authentication 类型为 BearerTokenAuthentication,其主体即由配置的 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> 返回的 OAuth2AuthenticatedPrincipal。最终,返回的 BearerTokenAuthentication 将由身份验证 Filter 设置在 SecurityContextHolder 上。

number 3 OpaqueTokenAuthenticationProvider introspects the opaque token and adds granted authorities using an <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>>. When authentication is successful, the Authentication that is returned is of type BearerTokenAuthentication and has a principal that is the OAuth2AuthenticatedPrincipal returned by the configured <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>>. Ultimately, the returned BearerTokenAuthentication will be set on the SecurityContextHolder by the authentication Filter.

Looking Up Attributes Post-Authentication

令牌通过认证后,BearerTokenAuthentication 实例将设置在 SecurityContext 中。

Once a token is authenticated, an instance of BearerTokenAuthentication is set in the SecurityContext.

这意味着当在配置中使用 @EnableWebMvc 时,它在 @Controller 方法中可用:

This means that it’s available in @Controller methods when using @EnableWebMvc in your configuration:

@GetMapping("/foo")
public String foo(BearerTokenAuthentication authentication) {
    return authentication.getTokenAttributes().get("sub") + " is the subject";
}

由于 BearerTokenAuthentication 持有 OAuth2AuthenticatedPrincipal,这意味着它也可以在控制器方法中使用:

Since BearerTokenAuthentication holds an OAuth2AuthenticatedPrincipal, that also means that it’s available to controller methods, too:

@GetMapping("/foo")
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
    return principal.getAttribute("sub") + " is the subject";
}

Looking Up Attributes Via SpEL

当然,这也意味着可以访问属性通过 SpEL。

Of course, this also means that attributes can be accessed via SpEL.

例如,如果使用 @EnableGlobalMethodSecurity 以便可以使用 @PreAuthorize 注解,则可以执行以下操作:

For example, if using @EnableGlobalMethodSecurity so that you can use @PreAuthorize annotations, you can do:

@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public String forFoosEyesOnly() {
    return "foo";
}

Overriding or Replacing Boot Auto Configuration

有 Spring Boot 从 Resource Server 创建的两个 @Bean

There are two `@Bean`s that Spring Boot generates on Resource Server’s behalf.

第一个 SecurityFilterChain 将应用程序配置为资源服务器。使用 Opaque 令牌时,该 SecurityFilterChain 类似于:

The first is a SecurityFilterChain that configures the app as a resource server. When use Opaque Token, this SecurityFilterChain looks like:

Default Opaque Token Configuration
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
    return http.build();
}

如果应用程序不公开 SecurityFilterChain bean,则 Spring Boot 会公开上述默认值。

If the application doesn’t expose a SecurityFilterChain bean, then Spring Boot will expose the above default one.

替换它就像在应用程序中公开 bean:

Replacing this is as simple as exposing the bean within the application:

Custom Opaque Token Configuration
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/messages/**").access(hasScope("message:read"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspector(myIntrospector())
                )
            );
        return http.build();
    }
}

上面要求任何从 /messages/ 开头的 URL 拥有 message:read 作用域。

The above requires the scope of message:read for any URL that starts with /messages/.

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

Methods on the oauth2ResourceServer DSL will also override or replace auto configuration.

例如,第二个 @Bean Spring Boot 创建的是 OpaqueTokenIntrospectorwhich decodes String tokens into validated instances of OAuth2AuthenticatedPrincipal

For example, the second @Bean Spring Boot creates is an OpaqueTokenIntrospector, oauth2resourceserver-opaque-architecture-introspector:

@Bean
public OpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

如果应用程序未公开 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> bean,则 Spring Boot 将公开上述默认 bean。

If the application doesn’t expose an <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> bean, then Spring Boot will expose the above default one.

其配置可以使用 introspectionUri()introspectionClientCredentials() 进行覆盖,或者使用 introspector() 进行替换。

And its configuration can be overridden using introspectionUri() and introspectionClientCredentials() or replaced using introspector().

如果应用程序未公开 OpaqueTokenAuthenticationConverter bean,则 spring-security 将构建 BearerTokenAuthentication

If the application doesn’t expose an OpaqueTokenAuthenticationConverter bean, then spring-security will build BearerTokenAuthentication.

或者,如果您根本未使用 Spring Boot,则所有这些组件(过滤器链、<<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> 和 OpaqueTokenAuthenticationConverter)都可以在 XML 中指定。

Or, if you’re not using Spring Boot at all, then all of these components - the filter chain, an <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> and an OpaqueTokenAuthenticationConverter can be specified in XML.

过滤器链是这样指定的:

The filter chain is specified like so:

Default Opaque Token Configuration
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <opaque-token introspector-ref="opaqueTokenIntrospector"
                authentication-converter-ref="opaqueTokenAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

以及 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> 类似于这样:

And the <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> like so:

Opaque Token Introspector
<bean id="opaqueTokenIntrospector"
        class="org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector">
    <constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.introspection_uri}"/>
    <constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.client_id}"/>
    <constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.client_secret}"/>
</bean>

以及 OpaqueTokenAuthenticationConverter 类似于这样:

And the OpaqueTokenAuthenticationConverter like so:

Opaque Token Authentication Converter
<bean id="opaqueTokenAuthenticationConverter"
        class="com.example.CustomOpaqueTokenAuthenticationConverter"/>

Using introspectionUri()

授权服务器的内省 URI 可以配置 as a configuration property 或在 DSL 中提供:

An authorization server’s Introspection Uri can be configured oauth2resourceserver-opaque-introspectionuri or it can be supplied in the DSL:

Introspection URI Configuration
@Configuration
@EnableWebSecurity
public class DirectlyConfiguredIntrospectionUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspectionUri("https://idp.example.com/introspect")
                    .introspectionClientCredentials("client", "secret")
                )
            );
        return http.build();
    }
}

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

Using introspectionUri() takes precedence over any configuration property.

Using introspector()

introspectionUri() 更强大的是 introspector(),它将完全替换 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> 的任何 Boot 自动配置:

More powerful than introspectionUri() is introspector(), which will completely replace any Boot auto configuration of <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>>:

Introspector Configuration
@Configuration
@EnableWebSecurity
public class DirectlyConfiguredIntrospector {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspector(myCustomIntrospector())
                )
            );
        return http.build();
    }
}

这在需要 authority mappingJWT revocationrequest timeouts 等更深入配置时很方便。

Exposing a OpaqueTokenIntrospector @Bean

或者,公开 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> @Beanintrospector() 的效果相同:

Or, exposing a <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> @Bean has the same effect as introspector():

@Bean
public OpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

Configuring Authorization

OAuth 2.0 内省端点通常会返回一个 scope 属性,指示它已被授予的范围(或权限),例如:

An OAuth 2.0 Introspection endpoint will typically return a scope attribute, indicating the scopes (or authorities) it’s been granted, for example:

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

如果是这种情况,资源服务器将尝试强制将这些作用域转换为已授予权限列表,用字符串“SCOPE_”作为每个作用域的前缀。

When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".

这意味着为了使用从不透明令牌派生的范围保护端点或方法,相应的表达式应包括此前缀:

This means that to protect an endpoint or method with a scope derived from an Opaque Token, the corresponding expressions should include this prefix:

Authorization Opaque Token Configuration
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class MappedAuthorities {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                .requestMatchers("/contacts/**").access(hasScope("contacts"))
                .requestMatchers("/messages/**").access(hasScope("messages"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
        return http.build();
    }
}

或者与此类似的方法安全性:

Or similarly with method security:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}

Extracting Authorities Manually

默认情况下,不透明令牌支持将从内省响应中提取范围声明并将其解析为单个 GrantedAuthority 实例。

By default, Opaque Token support will extract the scope claim from an introspection response and parse it into individual GrantedAuthority instances.

例如,如果内省响应是:

For example, if the introspection response were:

{
    "active" : true,
    "scope" : "message:read message:write"
}

那么,资源服务器将生成一个有两个授权的 Authentication,一个用于 message:read,另一个用于 message:write

Then Resource Server would generate an Authentication with two authorities, one for message:read and the other for message:write.

当然可以通过使用自定义的 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> 来自定义此操作,它查看属性集并以其自己的方式转换:

This can, of course, be customized using a custom <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> that takes a look at the attribute set and converts in its own way:

public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        return new DefaultOAuth2AuthenticatedPrincipal(
                principal.getName(), principal.getAttributes(), extractAuthorities(principal));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
        return scopes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

然后可以通过将此自定义内省器公开为 @Bean 来对其进行简单配置:

Thereafter, this custom introspector can be configured simply by exposing it as a @Bean:

@Bean
public OpaqueTokenIntrospector introspector() {
    return new CustomAuthoritiesOpaqueTokenIntrospector();
}

Configuring Timeouts

默认情况下,资源服务器针对协调授权服务器分别使用 30 秒的连接和套接字超时。

By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.

在某些情况下这可能太短。此外,它没有考虑到更复杂的模式,如回退和发现。

This may be too short in some scenarios. Further, it doesn’t take into account more sophisticated patterns like back-off and discovery.

为了调整资源服务器连接到授权服务器的方式,NimbusOpaqueTokenIntrospector 接受 RestOperations 的一个实例:

To adjust the way in which Resource Server connects to the authorization server, NimbusOpaqueTokenIntrospector accepts an instance of RestOperations:

@Bean
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) {
    RestOperations rest = builder
            .basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret())
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build();

    return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}

Using Introspection with JWTs

一个常见问题是不透明身份验证是否与 JWT 兼容。Spring Security 的不透明令牌支持旨在不关心令牌的格式 - 它会乐于将任何令牌传递到相应的不透明身份验证端点。

A common question is whether or not introspection is compatible with JWTs. Spring Security’s Opaque Token support has been designed to not care about the format of the token — it will gladly pass any token to the introspection endpoint provided.

因此,假设你有一个要求,要求你每次请求时都向授权服务器进行验证,以防 JWT 被吊销。

So, let’s say that you’ve got a requirement that requires you to check with the authorization server on each request, in case the JWT has been revoked.

即使你使用 JWT 格式作为令牌,你的验证方法也是内部验证,这意味着你需要执行:

Even though you are using the JWT format for the token, your validation method is introspection, meaning you’d want to do:

spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://idp.example.org/introspection
          client-id: client
          client-secret: secret

在这种情况下,结果的 AuthenticationBearerTokenAuthentication。相应 OAuth2AuthenticatedPrincipal 中的任何属性都将是不透明身份验证端点返回的任何内容。

In this case, the resulting Authentication would be BearerTokenAuthentication. Any attributes in the corresponding OAuth2AuthenticatedPrincipal would be whatever was returned by the introspection endpoint.

但假设在某种奇怪的情况下,不透明身份验证端点只返回令牌是否活动。那么怎么办?

But, let’s say that, oddly enough, the introspection endpoint only returns whether or not the token is active. Now what?

在这种情况下,你可以创建一个自定义的 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>>,它仍然可以访问该端点,但随后将返回的主体更新为具有 JWT 断言作为属性:

In this case, you can create a custom <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes:

public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor());

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        try {
            Jwt jwt = this.jwtDecoder.decode(token);
            return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES);
        } catch (JwtException ex) {
            throw new OAuth2IntrospectionException(ex);
        }
    }

    private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor<SecurityContext> {
    	JWTClaimsSet process(SignedJWT jwt, SecurityContext context)
                throws JOSEException {
            return jwt.getJWTClaimsSet();
        }
    }
}

然后可以通过将此自定义内省器公开为 @Bean 来对其进行简单配置:

Thereafter, this custom introspector can be configured simply by exposing it as a @Bean:

@Bean
public OpaqueTokenIntrospector introspector() {
    return new JwtOpaqueTokenIntrospector();
}

Calling a /userinfo Endpoint

一般来说,资源服务器并不关心底层用户,而更关心已授予的授权。

Generally speaking, a Resource Server doesn’t care about the underlying user, but instead about the authorities that have been granted.

也就是说,有时将授权声明重新关联到用户身上可能是有价值的。

That said, at times it can be valuable to tie the authorization statement back to a user.

如果应用程序也在使用 spring-security-oauth2-client,已设置了适当的 ClientRegistrationRepository,那么使用自定义的 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> 就可以非常容易地实现。以下实现执行三项操作:

If an application is also using spring-security-oauth2-client, having set up the appropriate ClientRegistrationRepository, then this is quite simple with a custom <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>>. This implementation below does three things:

  • Delegates to the introspection endpoint, to affirm the token’s validity

  • Looks up the appropriate client registration associated with the /userinfo endpoint

  • Invokes and returns the response from the /userinfo endpoint

public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService();

    private final ClientRegistrationRepository repository;

    // ... constructor

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
        Instant issuedAt = authorized.getAttribute(ISSUED_AT);
        Instant expiresAt = authorized.getAttribute(EXPIRES_AT);
        ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id");
        OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
        OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token);
        return this.oauth2UserService.loadUser(oauth2UserRequest);
    }
}

如果你没有使用 spring-security-oauth2-client,操作起来还是非常简单。你只需使用你自己的 WebClient 实例调用 /userinfo

If you aren’t using spring-security-oauth2-client, it’s still quite simple. You will simply need to invoke the /userinfo with your own instance of WebClient:

public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final WebClient rest = WebClient.create();

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
        return makeUserInfoRequest(authorized);
    }
}

无论哪种方式,创建好 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> 后,你都应该将其作为 @Bean 发布以覆盖默认值:

Either way, having created your <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>>, you should publish it as a @Bean to override the defaults:

@Bean
OpaqueTokenIntrospector introspector() {
    return new UserInfoOpaqueTokenIntrospector(...);
}