|
此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4! |
OAuth 2.0 资源服务器不透明Tokens
用于内省的极简依赖
如 JWT 的最小依赖 所述,资源服务器支持的大部分功能都包含在 spring-security-oauth2-resource-server 中。
然而,除非提供了自定义的 OpaqueTokenIntrospector,否则资源服务器将回退到 SpringOpaqueTokenIntrospector。
这意味着,只需 spring-security-oauth2-resource-server 即可构建一个支持不透明 Bearer Tokens的最小化可用资源服务器。
内省的最小配置
通常,不透明Tokens可以通过授权服务器托管的OAuth 2.0 自省端点进行验证。 当需要支持Tokens撤销时,这种方式非常有用。
使用Spring Boot时,将应用程序配置为使用Tokens自省(introspection)的资源服务器包含两个基本步骤: 首先,引入所需的依赖项;其次,指定自省端点的详细信息。
指定授权服务器
要指定内省端点的位置,只需执行以下操作:
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
其中 idp.example.com/introspect 是由您的授权服务器托管的Tokens自省端点,而 client-id 和 client-secret 是访问该端点所需的凭据。
资源服务器将使用这些属性进一步自我配置,并随后验证传入的 JWT。
| 使用内省(introspection)时,授权服务器的判断就是最终依据。 如果授权服务器返回该Tokens有效,那么它就是有效的。 |
就这样了!
运行时期望
一旦应用程序启动,资源服务器将尝试处理任何包含 Authorization: Bearer 请求头的请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指定了此方案,资源服务器将尝试根据 Bearer Token 规范处理该请求。
给定一个不透明Tokens(Opaque Token),资源服务器将
-
使用提供的凭据和Tokens查询所提供的内省端点
-
检查响应中是否包含
{ 'active' : true }属性 -
将每个 scope 映射到一个带有前缀
SCOPE_的权限
生成的 Authentication#getPrincipal 默认是一个 Spring Security OAuth2AuthenticatedPrincipal 对象,且 Authentication#getName 映射到Tokens的 sub 属性(如果存在的话)。
从这里,您可能想要跳转到:
不透明Tokens认证的工作原理
接下来,我们来看看 Spring Security 在基于 Servlet 的应用程序(例如我们刚刚看到的那个)中用于支持不透明Tokens身份验证的架构组件。
OpaqueTokenAuthenticationProvider 是一个利用 OpaqueTokenIntrospector 对不透明Tokens进行身份验证的 AuthenticationProvider 实现。
让我们来看看 OpaqueTokenAuthenticationProvider 在 Spring Security 中是如何工作的。
该图详细解释了来自 读取 Bearer Token 的图中 AuthenticationManager 的工作原理。
OpaqueTokenAuthenticationProvider 的使用
身份验证Filter来自读取 Bearer Tokens传递一个BearerTokenAuthenticationToken 至 AuthenticationManager由以下实现ProviderManager.
这ProviderManager已配置为使用认证提供者类型OpaqueTokenAuthenticationProvider.
OpaqueTokenAuthenticationProvider检查不透明Tokens并使用以下项添加授予的权限OpaqueTokenIntrospector.
当认证成功时,Authentication返回的内容类型为BearerTokenAuthentication并且拥有一个作为OAuth2AuthenticatedPrincipal由配置返回OpaqueTokenIntrospector以及包含至少一个权限的集合FACTOR_BEARER.
最终,返回的BearerTokenAuthentication将应用于SecurityContextHolder通过身份验证Filter.
认证后查找属性
一旦Tokens通过认证,BearerTokenAuthentication 的一个实例就会被设置到 SecurityContext 中。
这意味着在您的配置中使用 @Controller 时,它在 @EnableWebMvc 方法中是可用的:
-
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,这意味着它也可以在控制器方法中使用:
-
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"
}
通过 SpEL 查找属性
当然,这也意味着可以通过 SpEL 访问属性。
例如,如果使用 @EnableGlobalMethodSecurity 以便能够使用 @PreAuthorize 注解,你可以这样做:
-
Java
-
Kotlin
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public String forFoosEyesOnly() {
return "foo";
}
@PreAuthorize("principal?.attributes['sub'] == 'foo'")
fun forFoosEyesOnly(): String {
return "foo"
}
覆盖或替换 Boot 自动配置
Spring Boot 会代表资源服务器生成两个 @Bean。
第一个是配置应用程序作为资源服务器的 SecurityFilterChain。
当使用不透明Tokens(Opaque Token)时,该 SecurityFilterChain 如下所示:
-
Java
-
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.opaqueToken(Customizer.withDefaults())
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
return http.build()
}
如果应用程序未暴露一个 SecurityFilterChain bean,那么 Spring Boot 将会暴露上述默认的 bean。
只需在应用程序中暴露该 Bean 即可完成替换:
-
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 {
authorizeHttpRequests {
authorize("/messages/**", hasScope("SCOPE_message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
opaqueToken {
introspector = myIntrospector()
}
}
}
return http.build()
}
}
上述配置要求任何以 message:read 开头的 URL 都必须具有 /messages/ 范围。
oauth2ResourceServer DSL 中的方法也会覆盖或替换自动配置。
例如,Spring Boot 创建的第二个 @Bean 是一个 OpaqueTokenIntrospector,它将 String Tokens解码为 OAuth2AuthenticatedPrincipal 的有效实例:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector() {
return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(clientId).clientSecret(clientSecret).build();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(clientId).clientSecret(clientSecret).build()
}
如果应用程序未暴露 OpaqueTokenIntrospector Bean,则 Spring Boot 将暴露上述默认 Bean。
并且可以使用 introspectionUri() 和 introspectionClientCredentials() 覆盖其配置,或使用 introspector() 替换该配置。
如果应用程序未暴露一个 OpaqueTokenAuthenticationConverter Bean,那么 Spring Security 将构建 BearerTokenAuthentication。
或者,如果您根本不使用 Spring Boot,则所有这些组件——过滤器链、OpaqueTokenIntrospector 和 OpaqueTokenAuthenticationConverter——都可以在 XML 中指定。
过滤器链的指定方式如下:
-
Xml
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<opaque-token introspector-ref="opaqueTokenIntrospector"
authentication-converter-ref="opaqueTokenAuthenticationConverter"/>
</oauth2-resource-server>
</http>
就像这样:OpaqueTokenIntrospector:
-
Xml
<bean id="opaqueTokenIntrospector"
class="org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector">
<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:
-
Xml
<bean id="opaqueTokenAuthenticationConverter"
class="com.example.CustomOpaqueTokenAuthenticationConverter"/>
使用introspectionUri()
授权服务器的内省 URI 可以作为配置属性进行配置,也可以在 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 {
authorizeHttpRequests {
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.SpringOpaqueTokenIntrospector">
<constructor-arg value="https://idp.example.com/introspect"/>
<constructor-arg value="client"/>
<constructor-arg value="secret"/>
</bean>
使用 introspectionUri() 优先于任何配置属性。
使用introspector()
比 introspectionUri() 更强大的是 introspector(),它将完全替换任何 Boot 自动配置的 OpaqueTokenIntrospector:
-
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 {
authorizeHttpRequests {
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>
公开一个OpaqueTokenIntrospector @Bean
或者,暴露一个 OpaqueTokenIntrospector @Bean 的效果与 introspector() 相同:
@Bean
public OpaqueTokenIntrospector introspector() {
return return SpringOpaqueTokenIntrospector.withIntrospectionUri(introspectionUri)
.clientId(clientId).clientSecret(clientSecret).build();
}
配置授权
OAuth 2.0 自省(Introspection)端点通常会返回一个 scope 属性,用于指示已被授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器(Resource Server)会尝试将这些作用域(scopes)转换为一组授予的权限(granted authorities),并在每个作用域前加上字符串“SCOPE_”。
这意味着,若要使用从不透明Tokens(Opaque Token)派生的作用域来保护某个端点或方法,相应的表达式应包含此前缀:
-
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((oauth2) -> oauth2
.opaqueToken(Customizer.withDefaults())
);
return http.build();
}
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope
@Configuration
@EnableWebSecurity
class MappedAuthorities {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
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>
或者类似地使用方法安全:
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message?> {}
手动提取权限
默认情况下,不透明Tokens(Opaque Token)支持会从自省(introspection)响应中提取 scope 声明,并将其解析为单独的 GrantedAuthority 实例。
例如,如果内省响应为:
{
"active" : true,
"scope" : "message:read message:write"
}
然后资源服务器将生成一个包含两个权限的 Authentication,一个对应 message:read,另一个对应 message:write。
这当然可以通过自定义 OpaqueTokenIntrospector 进行定制,该自定义组件会查看属性集并以自己的方式进行转换:
-
Java
-
Kotlin
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build();
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 = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build()
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 的形式暴露出来即可完成配置:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return CustomAuthoritiesOpaqueTokenIntrospector()
}
配置超时设置
默认情况下,资源服务器在与授权服务器协调时,连接超时和套接字超时均设置为30秒。
在某些场景下,这可能太短了。 此外,它也没有考虑更复杂的模式,例如退避(back-off)和发现(discovery)。
要调整资源服务器连接授权服务器的方式,SpringOpaqueTokenIntrospector 接受一个 RestOperations 实例:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) {
RestOperations rest = builder
.basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret())
.connectTimeout(Duration.ofSeconds(60))
.readTimeout(Duration.ofSeconds(60))
.build();
return SpringOpaqueTokenIntrospector(introspectionUri, rest);
}
@Bean
fun introspector(builder: RestTemplateBuilder, properties: OAuth2ResourceServerProperties): OpaqueTokenIntrospector? {
val rest: RestOperations = builder
.basicAuthentication(properties.opaquetoken.clientId, properties.opaquetoken.clientSecret)
.connectTimeout(Duration.ofSeconds(60))
.readTimeout(Duration.ofSeconds(60))
.build()
return SpringOpaqueTokenIntrospector(introspectionUri, rest)
}
使用内省(Introspection)处理 JWT
一个常见的问题是:内省(introspection)是否与 JWT 兼容。 Spring Security 的不透明Tokens(Opaque Token)支持被设计为不关心Tokens的格式——它会愉快地将任何Tokens传递给所提供的内省端点。
假设你有一个需求,要求在每个请求时都向授权服务器进行检查,以确认 JWT 是否已被撤销。
尽管您使用的是 JWT 格式的Tokens,但您的验证方法是内省(introspection),这意味着您应该执行以下操作:
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: https://idp.example.org/introspection
client-id: client
client-secret: secret
在这种情况下,生成的 Authentication 将是 BearerTokenAuthentication。
相应 OAuth2AuthenticatedPrincipal 中的任何属性都将是由Tokens自省(introspection)端点返回的内容。
但是,假设奇怪的是,内省端点仅返回该Tokens是否处于激活状态。 现在该怎么办?
在这种情况下,您可以创建一个自定义的 OpaqueTokenIntrospector,它仍然会访问端点,但随后会将返回的主体更新为将 JWT 声明作为属性:
-
Java
-
Kotlin
public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build();
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 = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build()
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 的形式暴露出来即可完成配置:
-
Java
-
Kotlin
@Bean
public OpaqueTokenIntrospector introspector() {
return new JwtOpaqueTokenIntrospector();
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return JwtOpaqueTokenIntrospector()
}
调用/userinfo端点
一般来说,资源服务器并不关心底层用户,而是关心已被授予的权限。
话虽如此,有时将授权声明与用户关联起来也是很有价值的。
如果应用程序同时使用了 spring-security-oauth2-client,并且已配置好相应的 ClientRegistrationRepository,那么通过自定义 OpaqueTokenIntrospector 实现起来非常简单。
以下该实现完成了三件事:
-
委托给内省端点,以确认Tokens的有效性
-
查找与
/userinfo端点关联的相应客户端注册信息 -
调用
/userinfo端点并返回其响应
-
Java
-
Kotlin
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build();
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 = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build()
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,操作仍然非常简单。
你只需使用自己的 /userinfo 实例调用 WebClient 即可:
-
Java
-
Kotlin
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build();
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 = SpringOpaqueTokenIntrospector
.withIntrospectionUri("https://idp.example.org/introspect")
.clientId("client").clientSecret("secret").build()
private val rest: WebClient = WebClient.create()
override fun introspect(token: String): OAuth2AuthenticatedPrincipal {
val authorized = delegate.introspect(token)
return makeUserInfoRequest(authorized)
}
}
无论哪种情况,在创建您的 OpaqueTokenIntrospector 后,您应将其发布为 @Bean 以覆盖默认设置:
-
Java
-
Kotlin
@Bean
OpaqueTokenIntrospector introspector() {
return new UserInfoOpaqueTokenIntrospector(...);
}
@Bean
fun introspector(): OpaqueTokenIntrospector {
return UserInfoOpaqueTokenIntrospector(...)
}