# OAuth2.0资源服务器JWT
# JWT的最小依赖项
大多数资源服务器支持被收集到spring-security-oauth2-resource-server
中。然而,对JWTS的解码和验证的支持是spring-security-oauth2-jose
,这意味着这两个都是必要的,以便拥有一个支持JWT编码的承载令牌的工作资源服务器。
# JWTS的最小配置
当使用Spring Boot (opens new window)时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。
# 指定授权服务器
在 Spring 引导应用程序中,要指定使用哪个授权服务器,只需执行以下操作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
其中[https://idp.example.com/issuer](https://idp.example.com/issuer)
是授权服务器将发布的JWT令牌的iss
声明中包含的值。Resource Server将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的JWTS。
要使用issuer-uri 属性,还必须证明[https://idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration) 、[https://idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer) 或[https://idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer) 中的一个是授权服务器所支持的端点。此端点被称为提供者配置 (opens new window)端点或授权服务器元数据 (opens new window)端点。 |
---|
就这样!
# 创业期望
当使用此属性和这些依赖项时,Resource Server将自动配置自身以验证JWT编码的承载令牌。
它通过一个确定性的启动过程来实现这一点:
查询
jwks_url
属性的提供者配置或授权服务器元数据端点查询
jwks_url
端点以获得支持的算法将验证策略配置为查询
jwks_url
中找到的算法的有效公钥将验证策略配置为针对
[https://idp.example.com](https://idp.example.com)
的每个JWTSiss
索赔进行验证。
此过程的结果是,为了使资源服务器成功启动,授权服务器必须启动并接收请求。
如果在资源服务器查询时,授权服务器处于关闭状态(给定适当的超时),那么启动将失败。 |
---|
# 运行时期望
一旦启动应用程序,Resource Server将尝试处理任何包含Authorization: Bearer
报头的请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要表明了该方案,资源服务器就会尝试根据承载令牌规范来处理请求。
给定一个格式良好的JWT,资源服务器将:
根据在启动过程中从
jwks_url
端点获得并与JWT匹配的公钥验证其签名验证JWT的
exp
和nbf
时间戳以及JWT的iss
声明,并将每个作用域映射到一个前缀
SCOPE_
的权限。
由于授权服务器提供了可用的新密钥, Spring 安全性将自动旋转用于验证JWTS的密钥。 |
---|
默认情况下,生成的Authentication#getPrincipal
是 Spring securityJwt
对象,并且Authentication#getName
映射到JWT的sub
属性(如果存在)。
从这里开始,考虑跳到:
# JWT身份验证的工作方式
接下来,让我们看看 Spring Security在基于 Servlet 的应用程序中支持JWT (opens new window)身份验证所使用的体系结构组件,就像我们刚才看到的那样。
[JwtAuthenticationProvider
](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/oauth2/server/resource/authentication/jwtauthenticationprovider.html)是一个[AuthenticationProvider
](../.../././////认证/architectification/architecture.html# Servlet-authentification-authenticationprovider)实现,它利用了一个[<JwtDecoder
](#oaut
让我们来看看JwtAuthenticationProvider
在 Spring 安全性中是如何工作的。该图详细说明了AuthenticationManager
](.../../authentication/architecture.html# Servlet-authentication-authenticationmanager)在读取不记名令牌中的工作原理。
图1. JwtAuthenticationProvider
用法
来自读取不记名令牌的身份验证
Filter
将一个BearerTokenAuthenticationToken
传递到AuthenticationManager
,这是由[ProviderManager
](.../../authentication/architecture.html# Servlet-authentication-providermanager)实现的。
ProviderManager
被配置为使用身份验证提供者类型的JwtAuthenticationProvider
。
JwtAuthenticationProvider
使用[JwtDecoder
](#OAuth2Resourceserver-JWT-decoder)对Jwt
进行解码、验证和验证。
JwtAuthenticationProvider
然后使用[JwtAuthenticationConverter
](#OAuth2Resourceserver-JWT-Authorization-Extraction)将Jwt
转换为授予权限的Collection
。
当身份验证成功时,返回的[
Authentication
](.../../authentication/architecture.html# Servlet-authentication-authentication)类型为JwtAuthenticationToken
,并且具有一个主体,即由配置的JwtDecoder
返回的Jwt
。最终,返回的JwtAuthenticationToken
将由身份验证Filter
设置在[SecurityContextHolder
](.../authentication/architecture.html# Servlet-authentication-securitycontextholder)上。
# 直接指定授权服务器JWK设置的URI
如果授权服务器不支持任何配置端点,或者如果资源服务器必须能够独立于授权服务器启动,那么也可以提供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不是标准化的,但通常可以在授权服务器的文档中找到。 |
---|
因此,资源服务器不会在启动时对授权服务器进行ping。我们仍然指定issuer-uri
,以便Resource Server仍然在传入的JWTS上验证iss
声明。
也可以在DSL上直接提供此属性。 |
---|
# 覆盖或替换Boot Auto配置
有两个@Bean
s, Spring boot代表资源服务器生成。
第一个是将应用程序配置为资源服务器的WebSecurityConfigurerAdapter
。当包含spring-security-oauth2-jose
时,这个WebSecurityConfigurerAdapter
看起来是这样的:
例1.默认的JWT配置
Java
protected void configure(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
Kotlin
fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
如果应用程序不公开WebSecurityConfigurerAdapter
Bean,那么 Spring 引导将公开上面的默认引导。
替换它就像在应用程序中公开 Bean 一样简单:
例2.自定义JWT配置
Java
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(myConverter())
)
);
}
}
Kotlin
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("SCOPE_message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = myConverter()
}
}
}
}
}
对于任何以/messages/
开头的URL,上面要求message:read
的范围。
oauth2ResourceServer
DSL上的方法也将覆盖或替换自动配置。
例如,第二个@Bean
Spring 引导创建了一个JwtDecoder
,它[将String
令牌解码为Jwt
的验证实例](#OAuth2Resourceserver-jwt-architecture-jwtdecoder):
例3. JWT解码器
Java
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoders.fromIssuerLocation(issuerUri)
}
调用[JwtDecoders#fromIssuerLocation](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/jwt/JwtDecoders.html#fromIssuerLocation-java.lang.String-) 是调用提供程序配置或授权服务器元数据端点以派生JWK集URI的目的。 |
---|
如果应用程序不公开JwtDecoder
Bean,那么 Spring 引导将公开上面的默认引导。
并且其配置可以使用jwkSetUri()
重写或使用decoder()
替换。
或者,如果根本不使用 Spring boot,那么这两个组件--过滤器链和JwtDecoder
都可以用XML指定。
过滤链是这样指定的:
例4.默认的JWT配置
XML
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="jwtDecoder"/>
</oauth2-resource-server>
</http>
而JwtDecoder
就像这样:
例5. JWT解码器
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>
# `
可以配置授权服务器的JWK集URI作为配置属性,也可以在DSL中提供它:
例6. JWK设置URI配置
Java
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
}
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
}
}
XML
<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()
优先于任何配置属性。
# `
比jwkSetUri()
更强大的是decoder()
,它将完全取代[JwtDecoder
](#OAuth2Resourceserver-jwt-architecture-jwtdecoder)的任何引导自动配置:
例7. JWT解码器配置
Java
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
}
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
}
}
XML
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="myCustomDecoder"/>
</oauth2-resource-server>
</http>
当需要更深的配置时,比如validation、mapping或请求超时,这是很方便的。
# 曝光JwtDecoder``@Bean
或者,暴露[JwtDecoder
](#OAuth2Resourceserver-JWT-Architecture-JWTDecoder)@Bean
具有与decoder()
相同的效果:
Java
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
# 配置可信算法
默认情况下,NimbusJwtDecoder
以及资源服务器将仅使用RS256
信任和验证令牌。
你可以通过Spring Boot、NimbusJWTDecoder Builder或从JWK SET响应进行自定义。
# 通过 Spring 引导
设置算法的最简单方法是作为一个属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
# 使用构建器
不过,要获得更大的功率,我们可以使用带有NimbusJwtDecoder
的建造器:
Java
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build()
}
多次调用jwsAlgorithm
将配置NimbusJwtDecoder
来信任多个算法,如下所示:
Java
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,你可以调用jwsAlgorithms
:
Java
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}.build()
}
# 来自JWK SET响应
Spring 由于Security的JWT支持是基于Nimbus的,所以你也可以使用它的所有优秀功能。
例如,Nimbus有一个JWSKeySelector
实现,该实现将基于JWK设置的URI响应来选择一组算法。你可以使用它生成NimbusJwtDecoder
,如下所示:
Java
@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);
}
Kotlin
@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)
}
# 信任单一的非对称密钥
比支持具有JWK设置端点的资源服务器更简单的方法是对RSA公钥进行硬编码。公钥可以通过Spring Boot或使用构建器提供。
# 通过 Spring 引导
通过 Spring 引导指定一个键非常简单。密钥的位置可以这样指定:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
或者,为了允许更复杂的查找,你可以对RsaKeyConversionServicePostProcessor
进行后处理:
Java
@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
Kotlin
@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
return BeanFactoryPostProcessor { beanFactory ->
beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
.setResourceLoader(CustomResourceLoader())
}
}
指定你的密钥的位置:
key.location: hfds://my-key.pub
然后自动连接该值:
Java
@Value("${key.location}")
RSAPublicKey key;
Kotlin
@Value("\${key.location}")
val key: RSAPublicKey? = null
# 使用构建器
要直接连接RSAPublicKey
,只需使用适当的NimbusJwtDecoder
构建器,如下所示:
Java
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withPublicKey(this.key).build()
}
# 信任单一的对称密钥
使用单一的对称密钥也很简单。你可以简单地在SecretKey
中加载,并使用适当的NimbusJwtDecoder
构建器,如下所示:
Java
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(this.key).build();
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withSecretKey(key).build()
}
# 配置授权
从OAuth2.0授权服务器发出的JWT通常具有scope
或scp
属性,指示已授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,Resource Server将尝试强制将这些作用域放入一个已授予权限的列表中,并在每个作用域前加上字符串“scope_”。
这意味着,要使用从JWT派生的作用域来保护端点或方法,相应的表达式应该包括以下前缀:
例8.授权配置
Java
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
authorize("/messages/**", hasAuthority("SCOPE_messages"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
}
XML
<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
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }
# 手动提取权限
然而,在许多情况下,这种默认设置是不够的。例如,一些授权服务器不使用scope
属性,而是具有自己的自定义属性。或者,在其他时候,资源服务器可能需要将属性或属性的组合调整为内在化的权限。
为此, Spring Security附带JwtAuthenticationConverter
,它负责[将Jwt
转换为Authentication
](#OAuth2Resourceserver-JWT-Architecture-JWTAutoThenticationConverter)。默认情况下, Spring 安全性将用JwtAuthenticationProvider
的默认实例连接JwtAuthenticationConverter
。
作为配置JwtAuthenticationConverter
的一部分,你可以提供一个附属转换器,将其从Jwt
转换为授予权限的Collection
。
假设你的授权服务器在一个名为authorities
的自定义声明中与权威机构通信。在这种情况下,你可以配置[JwtAuthenticationConverter
](#OAuth2ResourceServer-JWT-Architecture-JWTAutoThenticationConverter)应该检查的声明,如下所示:
例9.当局声称配置
Java
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Kotlin
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
XML
<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>
你也可以将权限前缀配置为不同的。你可以将它更改为ROLE_
,而不是在每个权限前加上SCOPE_
,就像这样:
例10.权限前缀配置
Java
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Kotlin
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
XML
<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
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return new CustomAuthenticationToken(jwt);
}
}
// ...
@EnableWebSecurity
public class CustomAuthenticationConverterConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(new CustomAuthenticationConverter())
)
);
}
}
Kotlin
internal class CustomAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
override fun convert(jwt: Jwt): AbstractAuthenticationToken {
return CustomAuthenticationToken(jwt)
}
}
// ...
@EnableWebSecurity
class CustomAuthenticationConverterConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = CustomAuthenticationConverter()
}
}
}
}
}
# 配置验证
使用minimal Spring Boot configuration(表示授权服务器的发行者URI),Resource Server将默认验证iss
声明以及exp
和nbf
时间戳声明。
在需要定制验证的情况下,Resource Server附带两个标准验证器,并且还接受定制的OAuth2TokenValidator
实例。
# 自定义时间戳验证
JWT通常有一个有效窗口,窗口的开始在nbf
索赔中指示,结束在exp
索赔中指示。
然而,每个服务器都可能经历时钟漂移,这可能导致令牌在一台服务器上出现过期,而不是在另一台服务器上出现。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现令人心烦。
Resource Server使用JwtTimestampValidator
来验证令牌的有效性窗口,并且可以将其配置为clockSkew
以缓解上述问题:
Java
@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;
}
Kotlin
@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
}
默认情况下,Resource Server配置的时钟偏差为60秒。 |
---|
# 配置自定义验证器
使用OAuth2TokenValidator
API添加aud
声明的检查很简单:
Java
OAuth2TokenValidator<Jwt> audienceValidator() {
return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
Kotlin
fun audienceValidator(): OAuth2TokenValidator<Jwt?> {
return JwtClaimValidator<List<String>>(AUD) { aud -> aud.contains("messaging") }
}
或者,为了获得更多的控制,你可以实现自己的OAuth2TokenValidator
:
Java
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();
}
Kotlin
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
@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;
}
Kotlin
@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
}
# 配置索赔集映射
Spring 安全性使用Nimbus (opens new window)库来解析JWTS并验证其签名。因此, Spring 安全性取决于Nimbus对每个字段值的解释以及如何将每个字段值强制为 Java 类型。
例如,因为Nimbus保持 Java 7兼容,所以它不使用Instant
来表示时间戳字段。
而且完全有可能使用不同的库或用于JWT处理,这可能会做出自己的强制决策,需要进行调整。
或者,非常简单地说,出于特定于域的原因,资源服务器可能希望从JWT中添加或删除声明。
出于这些目的,Resource Server支持用MappedJwtClaimSetConverter
映射JWT索赔集。
# 定制单个索赔的转换
默认情况下,MappedJwtClaimSetConverter
将尝试强制将声明转换为以下类型:
Claim | Java 类型 |
---|---|
aud | Collection<String> |
exp | Instant |
iat | Instant |
iss | String |
jti | String |
nbf | Instant |
sub | String |
可以使用MappedJwtClaimSetConverter.withDefaults
配置单个索赔的转换策略:
Java
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
jwtDecoder.setClaimSetConverter(converter);
return jwtDecoder;
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
val converter = MappedJwtClaimSetConverter
.withDefaults(mapOf("sub" to this::lookupUserIdBySub))
jwtDecoder.setClaimSetConverter(converter)
return jwtDecoder
}
这将保留所有的默认值,除了它将覆盖sub
的默认索赔转换器。
# 增加索赔
MappedJwtClaimSetConverter
还可以用于添加自定义声明,例如,用于适应现有系统:
Java
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
Kotlin
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))
# 删除索赔
删除声明也很简单,使用相同的API:
Java
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
Kotlin
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))
# 重新命名索赔
在更复杂的场景中,例如一次查询多个声明或重命名一个声明,Resource Server接受实现Converter<Map<String, Object>, Map<String,Object>>
的任何类:
Java
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;
}
}
Kotlin
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
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
Kotlin
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
return jwtDecoder
}
# 配置超时
默认情况下,Resource Server使用30秒的连接和套接字超时来与授权服务器进行协调。
在某些情况下,这可能太短了。此外,它没有考虑到更复杂的模式,比如后退和发现。
要调整资源服务器连接到授权服务器的方式,NimbusJwtDecoder
接受RestOperations
的实例:
Java
@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
RestOperations rest = builder
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
return jwtDecoder;
}
Kotlin
@Bean
fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
val rest: RestOperations = builder
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build()
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build()
}
另外,默认情况下,Resource Server会在内存中缓存授权服务器的JWK设置5分钟,你可能需要对其进行调整。此外,它没有考虑更复杂的缓存模式,比如驱逐或使用共享缓存。
要调整资源服务器缓存JWK集的方式,NimbusJwtDecoder
接受Cache
的实例:
Java
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build();
}
Kotlin
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build()
}
当给定一个Cache
时,资源服务器将使用JWK设置的URI作为键,并使用JWK设置的JSON作为值。
Spring 不是缓存提供程序,因此你需要确保包含适当的依赖项,比如spring-boot-starter-cache 和你最喜欢的缓存提供程序。 |
---|
不管是套接字超时还是缓存超时,你可能希望直接使用Nimbus。要做到这一点,请记住 附带一个构造函数,该构造函数接受Nimbus的 JWTProcessor 。 |
---|