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-server
和 oauth2-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-id
和 client-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
-
Query the provided introspection endpoint using the provided credentials and the token
-
Inspect the response for an
{ 'active' : true }
attribute -
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
Usage 来自 Reading the Bearer Token 的身份验证
Filter
向 AuthenticationManager
传递了 BearerTokenAuthenticationToken
,而 ProviderManager
实现了 AuthenticationManager
。
The authentication
Filter
from oauth2resourceserver-authentication-bearertokenauthenticationfilter passes a BearerTokenAuthenticationToken
to the AuthenticationManager
which is implemented by ProviderManager
.
ProviderManager
配置为使用类型为 OpaqueTokenAuthenticationProvider
的 AuthenticationProvider。
The
ProviderManager
is configured to use an AuthenticationProvider of type OpaqueTokenAuthenticationProvider
.
OpaqueTokenAuthenticationProvider
通过 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> 内省不透明令牌并添加授予的权限。身份验证成功后,返回的 Authentication
类型为 BearerTokenAuthentication
,其主体即由配置的 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-introspector>> 返回的 OAuth2AuthenticatedPrincipal
。最终,返回的 BearerTokenAuthentication
将由身份验证 Filter
设置在 SecurityContextHolder
上。
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:
-
Java
-
Kotlin
@GetMapping("/foo")
public String foo(BearerTokenAuthentication authentication) {
return authentication.getTokenAttributes().get("sub") + " is the subject";
}
@GetMapping("/foo")
fun foo(authentication: BearerTokenAuthentication): String {
return authentication.tokenAttributes["sub"].toString() + " is the subject"
}
由于 BearerTokenAuthentication
持有 OAuth2AuthenticatedPrincipal
,这意味着它也可以在控制器方法中使用:
Since BearerTokenAuthentication
holds an OAuth2AuthenticatedPrincipal
, that also means that it’s available to controller methods, too:
-
Java
-
Kotlin
@GetMapping("/foo")
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
return principal.getAttribute("sub") + " is the subject";
}
@GetMapping("/foo")
fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): String {
return principal.getAttribute<Any>("sub").toString() + " 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:
-
Java
-
Kotlin
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public String forFoosEyesOnly() {
return "foo";
}
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
fun forFoosEyesOnly(): String {
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:
-
Java
-
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
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:
-
Java
-
Kotlin
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();
}
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/messages/**", hasScope("SCOPE_message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
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 创建的是 OpaqueTokenIntrospector
,which decodes String
tokens into validated instances of OAuth2AuthenticatedPrincipal
:
For example, the second @Bean
Spring Boot creates is an OpaqueTokenIntrospector
, oauth2resourceserver-opaque-architecture-introspector:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector() {
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return 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:
-
Xml
<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:
-
Xml
<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:
-
Xml
<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:
-
Java
-
Kotlin
-
Xml
@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();
}
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredIntrospectionUri {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspectionUri = "https://idp.example.com/introspect"
introspectionClientCredentials("client", "secret")
}
}
}
return http.build()
}
}
<bean id="opaqueTokenIntrospector"
class="org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector">
<constructor-arg value="https://idp.example.com/introspect"/>
<constructor-arg value="client"/>
<constructor-arg value="secret"/>
</bean>
使用 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>>:
-
Java
-
Kotlin
-
Xml
@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();
}
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredIntrospector {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspector = myCustomIntrospector()
}
}
}
return http.build()
}
}
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<opaque-token introspector-ref="myCustomIntrospector"/>
</oauth2-resource-server>
</http>
这在需要 authority mapping、JWT revocation 或 request timeouts 等更深入配置时很方便。
This is handy when deeper configuration, like oauth2resourceserver-opaque-authorization-extraction, oauth2resourceserver-opaque-jwt-introspector, or oauth2resourceserver-opaque-timeouts, is necessary.
Exposing a OpaqueTokenIntrospector
@Bean
或者,公开 <<`OpaqueTokenIntrospector`,oauth2resourceserver-opaque-architecture-introspector>> @Bean
与 introspector()
的效果相同:
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:
-
Java
-
Kotlin
-
Xml
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();
}
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope
@Configuration
@EnableWebSecurity
class MappedAuthorities {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/contacts/**", hasScope("contacts"))
authorize("/messages/**", hasScope("messages"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
return http.build()
}
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<opaque-token introspector-ref="opaqueTokenIntrospector"/>
</oauth2-resource-server>
</http>
或者与此类似的方法安全性:
Or similarly with method security:
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message?> {}
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:
-
Java
-
Kotlin
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());
}
}
class CustomAuthoritiesOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val principal: OAuth2AuthenticatedPrincipal = delegate.introspect(token)
return DefaultOAuth2AuthenticatedPrincipal(
principal.name, principal.attributes, extractAuthorities(principal))
}
private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection<GrantedAuthority> {
val scopes: List<String> = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE)
return scopes
.map { SimpleGrantedAuthority(it) }
}
}
然后可以通过将此自定义内省器公开为 @Bean
来对其进行简单配置:
Thereafter, this custom introspector can be configured simply by exposing it as a @Bean
:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return 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
:
-
Java
-
Kotlin
@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);
}
@Bean
fun introspector(builder: RestTemplateBuilder, properties: OAuth2ResourceServerProperties): OpaqueTokenIntrospector? {
val rest: RestOperations = builder
.basicAuthentication(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret)
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build()
return 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
在这种情况下,结果的 Authentication
是 BearerTokenAuthentication
。相应 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:
-
Java
-
Kotlin
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();
}
}
}
class JwtOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
private val jwtDecoder: JwtDecoder = NimbusJwtDecoder(ParseOnlyJWTProcessor())
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val principal = delegate.introspect(token)
return try {
val jwt: Jwt = jwtDecoder.decode(token)
DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES)
} catch (ex: JwtException) {
throw OAuth2IntrospectionException(ex.message)
}
}
private class ParseOnlyJWTProcessor : DefaultJWTProcessor<SecurityContext>() {
override fun process(jwt: SignedJWT, context: SecurityContext): JWTClaimsSet {
return jwt.jwtClaimsSet
}
}
}
然后可以通过将此自定义内省器公开为 @Bean
来对其进行简单配置:
Thereafter, this custom introspector can be configured simply by exposing it as a @Bean
:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector() {
return new JwtOpaqueTokenIntrospector();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return 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
-
Java
-
Kotlin
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);
}
}
class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
private val oauth2UserService = DefaultOAuth2UserService()
private val repository: ClientRegistrationRepository? = null
// ... constructor
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val authorized = delegate.introspect(token)
val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT)
val expiresAt: Instant? = authorized.getAttribute(EXPIRES_AT)
val clientRegistration: ClientRegistration = repository!!.findByRegistrationId("registration-id")
val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt)
val oauth2UserRequest = OAuth2UserRequest(clientRegistration, accessToken)
return 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
:
-
Java
-
Kotlin
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);
}
}
class UserInfoOpaqueTokenIntrospector : OpaqueTokenIntrospector {
private val delegate: OpaqueTokenIntrospector = NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret")
private val rest: WebClient = WebClient.create()
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val authorized = 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:
-
Java
-
Kotlin
@Bean
OpaqueTokenIntrospector introspector() {
return new UserInfoOpaqueTokenIntrospector(...);
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return UserInfoOpaqueTokenIntrospector(...)
}