OAuth 2.0 资源服务器 JWT
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
JWT的最小依赖
大多数资源服务器支持被收集到 spring-security-oauth2-resource-server
中。然而,对JWT的解码和验证的支持是在 spring-security-oauth2-jose
中,这意味着为了拥有一个支持JWT编码的 Bearer Token 的工作资源服务器,两者都是必要的。
JWT的最基本配置
在使用 Spring Boot 时,将一个应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖,其次,指定授权服务器的位置。
指定授权服务器
在Spring Boot应用程序中,要指定使用哪一个授权服务器,只需:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
其中 idp.example.com/issuer
,是授权服务器将发出的JWT令牌的 iss
claim 中包含的值。资源服务器将使用这个属性来进一步自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。
要使用 issuer-uri 属性,还必须是 idp.example.com/issuer/.well-known/openid-configuration 、idp.example.com/.well-known/openid-configuration/issuer 或 idp.example.com/.well-known/oauth-authorization-server/issuer 中的一个是授权服务器的支持端点。这个端点被称为 提供者配置 端点或 授权服务器元数据 端点。
|
就这样!
初始预期(Startup Expectations)
当使用该属性和这些依赖关系时,资源服务器将自动配置自己以验证JWT编码的Bearer Token。
它通过一个确定性的启动过程来实现这一点。
-
查询提供者配置或授权服务器元数据端点的
jwks_url
属性 -
查询
jwks_url
端点的支持算法 -
配置验证策略,以查询
jwks_url
中找到的算法的有效公钥。 -
配置验证策略,根据
idp.example.com
,验证每个JWTsiss
claim。
这个过程的一个结果是,授权服务器必须启动并接收请求,以便资源服务器成功启动。
如果在资源服务器查询时,授权服务器已经停机(给定适当的超时),那么启动将失败。 |
运行时预期(Runtime Expectations)
一旦应用程序启动,资源服务器将尝试处理任何包含 Authorization: Bearer
头的请求。
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指定了这个 scheme,资源服务器就会尝试根据 Bearer Token 规范来处理请求。
给定一个格式良好的JWT,资源服务器将:
-
根据启动期间从
jwks_url
端点获得的公钥验证其签名,并与JWT相匹配。 -
验证JWT的
exp
和nbf
时间戳以及JWT的iss
claim。 -
将每个 scope 映射到一个前缀为
SCOPE_
的授权。
随着授权服务器提供新的密钥,Spring Security将自动轮换用于验证JWTs的密钥。 |
由此产生的 Authentication#getPrincipal
默认为Spring Security Jwt
对象,Authentication#getName
映射到JWT的 sub
属性,如果有的话。
从这里,考虑跳转到:
JWT认证是如何工作的
接下来,让我们看看Spring Security用来支持基于servlet的应用程序中的 JWT 认证的架构组件,比如我们刚才看到的那个。
JwtAuthenticationProvider
是一个 AuthenticationProvider
实现,利用 JwtDecoder
和 JwtAuthenticationConverter
来验证JWT。
让我们来看看 JwtAuthenticationProvider
是如何在Spring Security中工作的。图中解释了数字中的 AuthenticationManager
在 读取 Bearer Token时的工作细节。
JwtAuthenticationProvider
Usage 读取 Bearer Token 的认证 Filter
将一个 BearerTokenAuthenticationToken
传递给由 ProviderManager
实现的认证管理器。
ProviderManager
被配置为使用一个 JwtAuthenticationProvider
类型的 AuthenticationProvider。
JwtAuthenticationProvider
使用 JwtDecoder
对 Jwt
进行解码、验证和确认。
JwtAuthenticationProvider
然后使用 JwtAuthenticationConverter
将 Jwt
转换为授予权限的集合(Collection
)。
当认证成功时,返回的 Authentication
是 JwtAuthenticationToken
类型的,并且有一个 principal 是由配置的 JwtDecoder
返回的 Jwt
。最终,返回的 JwtAuthenticationToken
将被认证 Filter
设置在 SecurityContextHolder
上。
直接指定授权服务器 JWK Set 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 Set uri不是标准化的,但通常可以在授权服务器的文档中找到。 |
因此,资源服务器在启动时将不会ping授权服务器。我们仍然指定了 issuer-uri
,这样资源服务器仍然会验证传入的JWT的 iss
claim。
This property can also be supplied directly on the DSL. |
提供 audiences
如前所述,issuer-uri
属性验证了 iss
claim;这是谁发送的JWT。
Boot 还具有用于验证 aud
claim 的 audiences
属性;这是 JWT 的发送对象。
资源服务器的 audience 可以这样表示:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
audiences: https://my-resource-server.example.com
如果需要,你也可以 通过编程添加 aud 验证。
|
结果是,如果 JWT 的 iss
claim 不是 idp.example.com
,且其 aud
claim 的列表中不包含 my-resource-server.example.com
,则验证将失败。
覆盖或取代启动自动配置
有两个 @Bean
是Spring Boot代表资源服务器生成的。
第一个是 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。
替换它就像在应用程序中公开该bean一样简单。
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(myConverter())
)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("SCOPE_message:read"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = myConverter()
}
}
}
return http.build()
}
}
以上要求任何以 /messages/
开头的URL的scope为 message:read
。
oauth2ResourceServer
DSL上的方法也将覆盖或取代自动配置。
例如,Spring Boot 创建的第二个 @Bean
是一个 JwtDecoder
,它 将 String token 解码为 Jwt
的验证实例。
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoders.fromIssuerLocation(issuerUri)
}
调用 JwtDecoders#fromIssuerLocation 就是调用提供者配置或授权服务器元数据端点,以便导出JWK Set Uri。
|
如果应用程序没有暴露一个 JwtDecoder
Bean,那么Spring Boot将暴露上述默认的 JwtDecoder
。
它的配置可以用 jwkSetUri()
重写或用 decoder()
替换。
或者,如果你根本没有使用Spring Boot,那么这两个组件—filter chain 和 JwtDecoder
都可以用XML指定。
过滤器链(filter chain)是这样指定的。
-
Java
<http>
<intercept-uri pattern="/**" access="authenticated"/>
<oauth2-resource-server>
<jwt decoder-ref="jwtDecoder"/>
</oauth2-resource-server>
</http>
而 JwtDecoder
是这样的:
-
Java
<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>
使用 jwkSetUri()
授权服务器的 JWK Set Uri 可以 作为一个配置属性 来配置,也可以在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()
优先于任何配置属性。
使用 decoder()
比 jwkSetUri()
更强大的是 decoder()
,它将完全取代 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,这很方便。
暴露一个 JwtDecoder
@Bean
或者,暴露一个JwtDecoder
@Bean
与 decoder()
有同样的效果。
你可以像这样用 jwkSetUri
构建一个:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
或者你可以使用 issuer,并让 NimbusJwtDecoder
在调用 build()
时查找 jwkSetUri
,如下所示:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
}
或者,如果默认值对你有效,你也可以使用 JwtDecoders
,除了配置 decoder 的验证器之外,它还可以完成上述工作:
-
Java
-
Kotlin
@Bean
public JwtDecoders jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): JwtDecoders {
return JwtDecoders.fromIssuerLocation(issuer)
}
配置受信任的算法
默认情况下,NimbusJwtDecoder
,以及资源服务器,将只信任和验证使用 RS256
的令牌。
你可以通过Spring Boot、NimbusJwtDecoder builder或从 JWK Set response 中定制。
通过Spring Boot实现
设置算法的最简单方法是配置一个属性。
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用 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()
}
来自 JWK Set 的响应
由于 Spring Security 的 JWT 支持基于 Nimbus,因此你也可以使用它的所有强大功能。
例如,Nimbus有一个 JWSKeySelector
实现,它将根据JWK Set URI的响应(Response)来选择算法集。你可以用它来生成一个 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)
}
信任单一非对称密钥
比用JWK Set端点支持资源服务器更简单的是硬编码一个RSA公钥。公钥可以通过Spring Boot或 使用 Builder来提供。
通过 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的位置。
key.location: hfds://my-key.pub
然后自动注入value。
-
Java
-
Kotlin
@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null
信任单一对称密钥
使用单一的对称密钥也很简单。你可以简单地加载你的 SecretKey
并使用适当的 NimbusJwtDecoder
builder,像这样。
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withSecretKey(key).build()
}
配置 Authorization(授权)
从OAuth 2.0授权服务器发出的JWT通常会有一个 scope
或 scp
属性,表明它被授予的scope(或权限),例如:
{ …, "scope" : "messages contacts"}
当出现这种情况时,资源服务器将尝试把这些scope强制性的成一个授予权限的列表,在每个scope前面加上 "SCOPE_" 字符串。
这意味着,要用从JWT派生的scope来保护一个端点或方法,相应的表达式应该包括这个前缀。
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.requestMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
authorize("/messages/**", hasAuthority("SCOPE_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>
或者类似于方法安全(method security)。
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }
手动提取权限
然而,在很多情况下,这个默认值是不够的。例如,有些授权服务器并不使用 scope
属性,而是有自己的自定义属性。或者,在其他时候,资源服务器可能需要将属性或属性的构成调整为内部化的授权。
为此,Spring Security提供了 JwtAuthenticationConverter
,它负责 将 Jwt
转换为 Authentication
。默认情况下,Spring Security会将 JwtAuthenticationProvider
与 JwtAuthenticationConverter
的默认实例连接起来。
作为配置 JwtAuthenticationConverter
的一部分,你可以提供一个附属的转换器,从 Jwt
到授予权限集合(Collection
)。
假设你的授权服务器在一个名为 authorities
的自定义 claim
中交流授权。在这种情况下,你可以配置 JwtAuthenticationConverter
应该检查的 claim
,像这样。
-
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>
你也可以把权限的前缀配置成不同的。你可以像这样把每个权限的前缀改为 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>
的类来完全替换converter。
-
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()
}
}
配置验证
使用 最小的Spring Boot配置,表明授权服务器的发行者URI,资源服务器将默认验证 iss
claim 以及 exp
和 nbf
时间戳 claim。
在需要定制验证的情况下,资源服务器会提供两个标准的验证器,同时也接受自定义的 OAuth2TokenValidator
实例。
自定义时间戳验证
JWT通常有一个有效期,开始时间在 nbf
claim 中指出,结束时间在 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秒。 |
配置自定义验证器(Validator)
使用 OAuth2TokenValidator
API,添加 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
实例即可。
-
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
}
如前所述,你可以在 Boot 中 配置 aud 验证。
|
配置 Claim 集映射
Spring Security使用 Nimbus 库来解析JWT并验证其签名。因此,Spring Security受制于Nimbus对每个字段值的解释,以及如何将每个字段强制成一个Java类型。
例如,由于Nimbus仍然与Java 7兼容,它不使用 Instant
来表示时间戳字段。
而且,完全可以使用不同的库或用于JWT处理,它可能会做出自己的强制决定,需要进行调整。
或者,很简单,资源服务器可能想出于特定领域的原因从JWT中添加或删除claim。
为了这些目的,资源服务器支持用 MappedJwtClaimSetConverter
映射JWT请求集。
定制单一 Claim 的转换
默认情况下,MappedJwtClaimSetConverter
将试图将 claim 强制为以下类型。
Claim |
Java 类型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可以使用 MappedJwtClaimSetConverter.withDefaults
来配置单个 claim 的转换策略。
-
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
的默认 claim
converter。
添加 Claim
MappedJwtClaimSetConverter
也可以用来添加一个自定义的 claim,例如,为了适应现有的系统。
-
Java
-
Kotlin
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))
删除 Claim
删除claim也很简单,使用相同的API。
-
Java
-
Kotlin
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))
重命名 Claim
在更复杂的情况下,如同时咨询多个claim或重命名一个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
}
配置超时
默认情况下,资源服务器使用连接和套接字超时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 Set Uri作为键,JWK Set JSON作为值。
Spring不是一个缓存提供者,所以你需要确保包含适当的依赖关系,比如 spring-boot-starter-cache 和你喜欢的缓存提供者。
|
无论是套接字还是缓存超时,你可能反而想直接与Nimbus合作。要做到这一点,请记住 NimbusJwtDecoder 有一个构造函数,它接收 Nimbus 的 JWTProcessor 。
|