OAuth 2.0 Resource Server JWT
Minimal Dependencies for JWT
大部分资源服务器支持收集到 spring-security-oauth2-resource-server
中。然而,解码和验证 JWT 的支持在 spring-security-oauth2-jose
中,这意味着两者对于拥有支持 JWT 编码承载令牌的可用资源服务器都是必需的。
Minimal Configuration for JWTs
当使用 Spring Boot 时,将应用程序配置为资源服务器包括两个基本步骤。首先,包含所需的依赖项,其次,指示授权服务器的位置。
Specifying the Authorization Server
在Spring Boot应用中,若要指定要使用哪一个授权服务器,只需执行以下操作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
其中,`https://idp.example.com/issuer`是授权服务器要颁发的JWT令牌中包含的`iss`声明中的值。资源服务器会使用此属性进行进一步的自我配置、发现授权服务器的公钥并随后验证入站JWT。
若要使用 |
这就是全部!
Startup Expectations
当使用此属性和这些依赖项时,资源服务器将自动配置其自身来验证经过JWT编码的Bearer令牌。
它通过确定性的启动过程实现这一点:
-
查询提供程序配置或授权服务器元数据端点以获取
jwks_url
属性 -
查询
jwks_url
端点以获取受支持的算法 -
使用
jwks_url
为算法的有效公钥配置验证策略,以查询算法的有效公钥 -
配置验证策略,以针对
https://idp.example.com
验证每个 JWT 令牌的iss
声明。
这个过程的后果是授权服务器必须处于启动状态并接收请求,以便资源服务器成功启动。
如果资源服务器在查询它(给定适当的超时)时,授权服务器关闭,然后启动将失败。 |
Runtime Expectations
应用程序启动后,资源服务器将尝试处理包含`Authorization: Bearer`标头的任何请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指出了此方案,资源服务器会尝试根据Bearer令牌规范处理请求。
鉴于经过格式良好的JWT,资源服务器将:
-
针对启动期间从
jwks_url
端点获取并与 JWT 相匹配的公钥验证其签名 -
验证 JWT 令牌的
exp
和nbf
时间戳以及 JWT 令牌的iss
声明,并且, -
用前缀
SCOPE_
将每个范围映射到一个权限。
当授权服务器提供新密钥时,Spring Security 将自动轮换用于验证 JWT 的密钥。 |
产生的`Authentication#getPrincipal`在默认情况下是Spring Security`Jwt`对象,如果存在,`Authentication#getName`映射到JWT的`sub`属性。
从此处,考虑跳转到:
How JWT Authentication Works
接下来,让我们看看 Spring Security 用于支持类似于我们刚才看到的基于 servlet 的应用程序中的 JWT 身份验证的体系结构组件。
{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[JwtAuthenticationProvider
] 是一种 AuthenticationProvider
实现,它利用 <<`JwtDecoder`,oauth2resourceserver-jwt-decoder>> 和 <<`JwtAuthenticationConverter`,oauth2resourceserver-jwt-authorization-extraction>> 对 JWT 进行身份验证。
让我们来看看 JwtAuthenticationProvider
如何在 Spring Security 中工作。此图说明了 Reading the Bearer Token 图中的 AuthenticationManager
如何工作的详细信息。
JwtAuthenticationProvider
Usage 来自 Reading the Bearer Token 的身份验证 Filter
向 AuthenticationManager
传递了 BearerTokenAuthenticationToken
,而 ProviderManager
实现了 AuthenticationManager
。
已配置为使用类型为 JwtAuthenticationProvider
的 AuthenticationProvider。
JwtAuthenticationProvider
使用 <<`JwtDecoder`,oauth2resourceserver-jwt-decoder>> 解码、验证和验证 Jwt
。
JwtAuthenticationProvider
然后使用 <<`JwtAuthenticationConverter`,oauth2resourceserver-jwt-authorization-extraction>> 将 Jwt
转换为被授予权限的 Collection
。
当身份验证成功时,返回的 Authentication
是 JwtAuthenticationToken
类型,并且有一个主体,即由已配置的 JwtDecoder
返回的 Jwt
。最终,返回的 JwtAuthenticationToken
将由身份验证 Filter
设置在 SecurityContextHolder
上。
Specifying the Authorization Server JWK Set Uri Directly
如果授权服务器不支持任何配置端点,或者如果资源服务器必须能够独立于授权服务器启动,那么可以提供`jwk-set-uri`:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
JWK 集 uri 没有标准化,但通常可以在授权服务器的文档中找到它 |
因此,Resource Server 没有在启动时检查授权服务器。我们仍然指定 issuer-uri
,以便 Resource Server 仍然验证传入 JWT 的 iss
声明。
此属性也可以直接在 DSL 上提供。 |
Supplying Audiences
如已经看到,<<`issuer-uri` 属性验证 iss
声明,_指定_授权服务器>>; 这是发送 JWT 的对象。
Boot 也具有 audiences
属性用于验证 aud
声明;这是 JWT 的发送对象。
可以如下所示指示资源服务器的受众:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
audiences: https://my-resource-server.example.com
如果需要,你也可以添加 the |
结果为,如果 JWT 的 iss
声明不是 https://idp.example.com
,并且它的 aud
声明在列表中不包含 https://my-resource-server.example.com
,则验证将失败。
Overriding or Replacing Boot Auto Configuration
有 Spring Boot 从 Resource Server 创建的两个 @Bean
。
第一个是将应用程序配置为资源服务器的 SecurityFilterChain
。在包含 spring-security-oauth2-jose
时,此 SecurityFilterChain
如下所示:
-
Java
-
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}
如果应用程序不公开 SecurityFilterChain
bean,则 Spring Boot 会公开上述默认值。
替换它就像在应用程序中公开 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
.jwt(jwt -> jwt
.jwtAuthenticationConverter(myConverter())
)
);
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("message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = myConverter()
}
}
}
return http.build()
}
}
上面要求任何从 /messages/
开头的 URL 拥有 message:read
作用域。
oauth2ResourceServer
DSL 上的方法也会覆盖或替换自动配置。
例如,Spring Boot 创建的第二个 @Bean
是 JwtDecoder
,它 decodes String
tokens into validated instances of Jwt
:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoders.fromIssuerLocation(issuerUri)
}
调用 |
如果应用程序不公开 JwtDecoder
bean,则 Spring Boot 会公开上述默认值。
并且可以使用 jwkSetUri()
替换配置或使用 decoder()
。
或者,如果您根本不使用 Spring Boot,则可以在 XML 中指定这两个组件 - 过滤器链和 JwtDecoder
。
过滤器链是这样指定的:
-
Xml
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="jwtDecoder"/>
</oauth2-resource-server>
</http>
并且 JwtDecoder
如下所示:
-
Xml
<bean id="jwtDecoder"
class="org.springframework.security.oauth2.jwt.JwtDecoders"
factory-method="fromIssuerLocation">
<constructor-arg value="${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}"/>
</bean>
Using jwkSetUri()
授权服务器的 JWK 设置 URI 可以在 as a configuration property 配置,或者它可以在 DSL 中提供:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
return http.build()
}
}
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.com/.well-known/jwks.json"/>
</oauth2-resource-server>
</http>
使用 jwkSetUri()
优先于任何配置属性。
Using decoder()
比 jwkSetUri()
更强大的是 decoder()
,它将完全替换 <<`JwtDecoder`,oauth2resourceserver-jwt-architecture-jwtdecoder>> 的任何 Boot 自动配置:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
return http.build()
}
}
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="myCustomDecoder"/>
</oauth2-resource-server>
</http>
当需要深入配置,诸如 validation、mapping 或 request timeouts 时,这非常方便。
Exposing a JwtDecoder
@Bean
或者,公开一个 <<`JwtDecoder`,oauth2resourceserver-jwt-architecture-jwtdecoder>> @Bean
的效果与 decoder()
相同。您可以用 jwkSetUri
构造一个,如下所示:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
或者,您可以使用颁发者并让 NimbusJwtDecoder
在调用 build()
时查找 jwkSetUri
,如下所示:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
}
或者,如果默认设置对您有效,您还可以使用 JwtDecoders
,它除了配置解码器的验证器外还会执行上述操作:
-
Java
-
Kotlin
@Bean
public JwtDecoders jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): JwtDecoders {
return JwtDecoders.fromIssuerLocation(issuer)
}
Configuring Trusted Algorithms
默认情况下,NimbusJwtDecoder
及由此及出的资源服务器只会使用 RS256
信任并验证令牌。
您可以通过 Spring Boot、the NimbusJwtDecoder builder 或 JWK Set response 来自定义此操作。
Via Spring Boot
设置算法的最简单方法是用作属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
Using a Builder
然而,为了获得更大的能力,我们可以使用 NimbusJwtDecoder
附带的生成器:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
}
多次调用 jwsAlgorithm
将配置 NimbusJwtDecoder
以信任一种以上的算法,如下所示:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,您可以调用 jwsAlgorithms
:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}.build()
}
From JWK Set response
由于 Spring Security 的 JWT 支持基于 Nimbus,因此您也可以使用它所有出色的功能。
例如,Nimbus 有一个 JWSKeySelector
实现,它将根据 JWK 设置 URI 响应选择一组算法。您可以用它生成 NimbusJwtDecoder
,如下所示:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
// makes a request to the JWK Set endpoint
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
fun jwtDecoder(): JwtDecoder {
// makes a request to the JWK Set endpoint
val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
jwtProcessor.jwsKeySelector = jwsKeySelector
return NimbusJwtDecoder(jwtProcessor)
}
Trusting a Single Asymmetric Key
比使用 JWK 设置端点支持资源服务器的更简单的做法是硬编码 RSA 公钥。可以通过 Spring Boot 或通过 Using a Builder 提供公钥。
Via Spring Boot
通过 Spring Boot 指定密钥非常简单。密钥的位置可以如下指定:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
或者,为了允许更复杂的查找,您可以对 RsaKeyConversionServicePostProcessor
进行后处理:
-
Java
-
Kotlin
@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
return BeanFactoryPostProcessor { beanFactory ->
beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
.setResourceLoader(CustomResourceLoader())
}
}
指定您的密钥位置:
key.location: hfds://my-key.pub
然后自动装配值:
-
Java
-
Kotlin
@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null
Trusting a Single Symmetric Key
使用单一对称密钥也很简单。您只需加载 SecretKey
并使用适当的 NimbusJwtDecoder
生成器,如下所示:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withSecretKey(key).build()
}
Configuring Authorization
OAuth 2.0 授权服务器颁发的 JWT 通常具有 scope
或 scp
特性,表示已授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
如果是这种情况,资源服务器将尝试强制将这些作用域转换为已授予权限列表,用字符串“SCOPE_”作为每个作用域的前缀。
这意味着,要使用源自 JWT 的作用域保护端点或方法,相应的表达式应包括此前缀:
-
Java
-
Kotlin
-
Xml
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;
@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/contacts/**").access(hasScope("contacts"))
.requestMatchers("/messages/**").access(hasScope("messages"))
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/contacts/**", hasScope("contacts"))
authorize("/messages/**", hasScope("messages"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"/>
</oauth2-resource-server>
</http>
或者与此类似的方法安全性:
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }
Extracting Authorities Manually
但是,在多种情况下,此默认值都是不够的。例如,一些授权服务器未使用 scope
属性,而是有自己的自定义属性。或者,在其他时候,资源服务器可能需要根据内部化权限调整属性或一组属性。
为此,Spring Security 附带了 JwtAuthenticationConverter
,它负责 converting a Jwt
into an Authentication
。默认情况下,Spring Security 会用 JwtAuthenticationConverter
的默认实例连接 JwtAuthenticationProvider
。
作为配置 JwtAuthenticationConverter
的一部分,您可以提供一个从 Jwt
到 Collection
的已授予权限的子转换器。
假设您的授权服务器在名为 authorities
的自定义声明中传达权限。在这种情况下,您可以配置 <<`JwtAuthenticationConverter`,oauth2resourceserver-jwt-architecture-jwtauthenticationconverter>> 应检查的声明,如下所示:
-
Java
-
Kotlin
-
Xml
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
</oauth2-resource-server>
</http>
<bean id="jwtAuthenticationConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
<property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>
<bean id="jwtGrantedAuthoritiesConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
<property name="authoritiesClaimName" value="authorities"/>
</bean>
您还可以将权限前缀配置为有所不同。您可以将前缀从 SCOPE_
更改为 ROLE_
,如下所示,而不仅仅是用 SCOPE_
作为每个权限的前缀:
-
Java
-
Kotlin
-
Xml
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
<http>
<intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
<intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
<oauth2-resource-server>
<jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
</oauth2-resource-server>
</http>
<bean id="jwtAuthenticationConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
<property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>
<bean id="jwtGrantedAuthoritiesConverter"
class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
<property name="authorityPrefix" value="ROLE_"/>
</bean>
或者,您可以通过调用 JwtGrantedAuthoritiesConverter#setAuthorityPrefix("")
完全移除前缀。
为了更灵活,DSL 支持完全用任何实现 Converter<Jwt, AbstractAuthenticationToken>
的类替换转换器:
-
Java
-
Kotlin
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return new CustomAuthenticationToken(jwt);
}
}
// ...
@Configuration
@EnableWebSecurity
public class CustomAuthenticationConverterConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(new CustomAuthenticationConverter())
)
);
return http.build();
}
}
internal class CustomAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
override fun convert(jwt: Jwt): AbstractAuthenticationToken {
return CustomAuthenticationToken(jwt)
}
}
// ...
@Configuration
@EnableWebSecurity
class CustomAuthenticationConverterConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = CustomAuthenticationConverter()
}
}
}
return http.build()
}
}
Configuring Validation
通过使用 minimal Spring Boot configuration 指出授权服务器的发行人 URI,资源服务器将默认验证 iss
声明以及 exp
和 nbf
时间戳声明。
在需要对验证进行自定义的情况下,资源服务器附带两个标准验证器,并且还接受自定义 OAuth2TokenValidator
实例。
Customizing Timestamp Validation
JWT 通常有一个有效期窗口,窗口的开始用 nbf
声明表示,窗口的结束用 exp
声明表示。
但是,每个服务器都可能遇到时钟漂移,这可能导致某个服务器认为令牌已过期,而另一个服务器却没有。这可能会导致一些实现感到心烦,随着协作服务器的数量在分布式系统中增加,可能会出现这种情况。
资源服务器使用 JwtTimestampValidator
验证令牌的有效期窗口,并且可以为其配置 clockSkew
以缓解上述问题:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new JwtIssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder
val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
JwtTimestampValidator(Duration.ofSeconds(60)),
JwtIssuerValidator(issuerUri))
jwtDecoder.setJwtValidator(withClockSkew)
return jwtDecoder
}
默认情况下,资源服务器配置为时钟偏移 60 秒。 |
Configuring a Custom Validator
使用 OAuth2TokenValidator
API,只需添加一个对 the aud
claim 的检查非常简单:
-
Java
-
Kotlin
OAuth2TokenValidator<Jwt> audienceValidator() {
return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
fun audienceValidator(): OAuth2TokenValidator<Jwt?> {
return JwtClaimValidator<List<String>>(AUD) { aud -> aud.contains("messaging") }
}
或者,为了获得更多控制权,您可以实现自己的 OAuth2TokenValidator
:
-
Java
-
Kotlin
static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null);
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
// ...
OAuth2TokenValidator<Jwt> audienceValidator() {
return new AudienceValidator();
}
internal class AudienceValidator : OAuth2TokenValidator<Jwt> {
var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null)
override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
return if (jwt.audience.contains("messaging")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(error)
}
}
}
// ...
fun audienceValidator(): OAuth2TokenValidator<Jwt> {
return AudienceValidator()
}
然后,要添加到资源服务器中,只需要指定 <<`JwtDecoder`,oauth2resourceserver-jwt-architecture-jwtdecoder>> 实例即可:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder
val audienceValidator = audienceValidator()
val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}
如前所述,你可以改为 configure |
Configuring Claim Set Mapping
Spring Security 使用 Nimbus 库来解析 JWT 并验证其签名。因此,Spring Security 受 Nimbus 对每个字段值的解释以及如何将其强制转换为 Java 类型的限制。
例如,因为 Nimbus 仍然兼容 Java 7,所以它不使用 Instant
来表示时间戳字段。
而且完全有可能使用不同的库或进行 JWT 处理,这可能会做出需要调整的自己的强制转换决策。
或者,从根本上来说,资源服务器可能希望基于特定于域的原因向 JWT 添加或从中移除声明。
出于这些目的,资源服务器支持使用 MappedJwtClaimSetConverter
映射 JWT 声明集。
Customizing the Conversion of a Single Claim
默认情况下,MappedJwtClaimSetConverter
将尝试将声明强制转换为以下类型:
Claim |
Java Type |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可以使用 MappedJwtClaimSetConverter.withDefaults
配置单个声明的转换策略:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
jwtDecoder.setClaimSetConverter(converter);
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
val converter = MappedJwtClaimSetConverter
.withDefaults(mapOf("sub" to this::lookupUserIdBySub))
jwtDecoder.setClaimSetConverter(converter)
return jwtDecoder
}
这将保留所有默认值,但它将覆盖 sub
的默认声明转换器。
Adding a Claim
MappedJwtClaimSetConverter
也可以用于添加自定义声明,例如,以适应现有系统:
-
Java
-
Kotlin
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))
Removing a Claim
并且移除声明也很简单,使用同样的 API:
-
Java
-
Kotlin
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))
Renaming a Claim
在更加复杂的情景中,比如同时查阅多个声明或重命名一个声明,资源服务器接受实现 Converter<Map<String, Object>, Map<String,Object>>
的任何类:
-
Java
-
Kotlin
public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
private final MappedJwtClaimSetConverter delegate =
MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
public Map<String, Object> convert(Map<String, Object> claims) {
Map<String, Object> convertedClaims = this.delegate.convert(claims);
String username = (String) convertedClaims.get("user_name");
convertedClaims.put("sub", username);
return convertedClaims;
}
}
class UsernameSubClaimAdapter : Converter<Map<String, Any?>, Map<String, Any?>> {
private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap())
override fun convert(claims: Map<String, Any?>): Map<String, Any?> {
val convertedClaims = delegate.convert(claims)
val username = convertedClaims["user_name"] as String
convertedClaims["sub"] = username
return convertedClaims
}
}
然后,可以像往常一样提供实例:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
return jwtDecoder
}
Configuring Timeouts
默认情况下,资源服务器针对协调授权服务器分别使用 30 秒的连接和套接字超时。
在某些情况下这可能太短。此外,它没有考虑到更复杂的模式,如回退和发现。
要调整资源服务器连接到授权服务器的方式,NimbusJwtDecoder
接受一个 RestOperations
实例:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
RestOperations rest = builder
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build();
return jwtDecoder;
}
@Bean
fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
val rest: RestOperations = builder
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build()
return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build()
}
此外,资源服务器默认将授权服务器的 JWK 集缓存在内存中 5 分钟,这可能是你希望调整的。此外,它没有考虑到更复杂的缓存模式,比如逐出或使用共享缓存。
要调整资源服务器缓存 JWK 集的方式,NimbusJwtDecoder
接受一个 Cache
实例:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withIssuerLocation(issuer)
.cache(cacheManager.getCache("jwks"))
.build();
}
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(issuer)
.cache(cacheManager.getCache("jwks"))
.build()
}
提供 Cache
时,资源服务器将使用 JWK 集 Uri 作为键,使用 JWK 集 JSON 作为值。
Spring 不是缓存提供程序,所以你需要确保包含适当的依赖项,如 |
无论是套接字还是缓存超时,你可能反而想直接使用 Nimbus。为此,请记住 |