OAuth 2.0 资源服务器 JWT
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](https://idp.example.com/issuer) 是授权服务器将颁发的 JWT 令牌中 iss 声明所包含的值。资源服务器将使用此属性进行进一步的自配置、发现授权服务器的公钥,并随后验证传入的 JWT。
要使用 issuer-uri 属性,还必须满足以下条件之一:[idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration)、[idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer) 或 [idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer) 是授权服务器支持的端点。此端点被称为 Provider Configuration 端点或 Authorization Server Metadata 端点。
就是这样!
启动预期
当使用此属性及这些依赖项时,资源服务器将自动配置自身以验证JWT编码的Bearer令牌。
它通过一个确定性的启动过程实现这一点:
-
查询提供者配置或授权服务器元数据端点以获取
jwks_url属性 -
查询
jwks_url端点以获取支持的算法 -
配置验证策略,使其查询
jwks_url以获取所发现算法的有效公钥 -
配置验证策略,使其针对
[idp.example.com](https://idp.example.com)验证每个 JWT 的iss声明
这一过程的一个结果是,授权服务器必须处于运行状态并能够接收请求,资源服务器才能成功启动。
如果资源服务器在查询授权服务器时,授权服务器处于宕机状态(在适当的超时设置下),那么启动将会失败。
运行时预期
一旦应用程序启动,资源服务器将尝试处理任何包含 Authorization: Bearer 头的请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要此方案被指定,资源服务器将尝试根据Bearer Token规范处理请求。
给定一个格式正确的JWT,资源服务器将:
-
使用启动时从
jwks_url端点获取并与 JWT 匹配的公钥验证其签名 -
验证 JWT 的
exp和nbf时间戳以及 JWT 的iss声明,并且 -
将每个 scope 映射到一个带有
SCOPE_前缀的权限。
随着授权服务器提供新的密钥,Spring Security 将自动轮换用于验证 JWT 的密钥。
默认情况下,生成的 Authentication#getPrincipal 是一个 Spring Security 的 Jwt 对象,而 Authentication#getName 则映射到 JWT 的 sub 属性(如果该属性存在的话)。
从这里开始,可以考虑跳转到:
JWT 认证的工作原理
接下来,我们来看看 Spring Security 在基于 servlet 的应用程序(例如我们刚刚看到的那个)中用于支持 JWT 身份验证的架构组件。
JwtAuthenticationProvider 是一个 AuthenticationProvider 实现,它利用 JwtDecoder 和 JwtAuthenticationConverter 来验证 JWT。
让我们来看看 JwtAuthenticationProvider 在 Spring Security 中是如何工作的。该图详细解释了 读取承载令牌 章节图示中的 AuthenticationManager 是如何工作的。

图 1. JwtAuthenticationProvider 使用示例
1 来自读取承载令牌的身份验证 Filter 会将一个 BearerTokenAuthenticationToken 传递给由 ProviderManager 实现的 AuthenticationManager。
2 ProviderManager 被配置为使用类型为 JwtAuthenticationProvider 的 AuthenticationProvider。
3 JwtAuthenticationProvider 使用 JwtDecoder 对 Jwt 进行解码、验证和校验。
4 随后,JwtAuthenticationProvider 使用 JwtAuthenticationConverter 将 Jwt 转换为已授权权限的 Collection。
5 当认证成功时,返回的 Authentication 类型为 JwtAuthenticationToken,其主体(principal)是由配置的 JwtDecoder 返回的 Jwt,并且拥有一组权限(authorities),其中至少包含 FACTOR_BEARER。最终,返回的 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 声明。
此属性也可直接在 DSL 上提供。
提供受众群体
正如之前所见,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
如有需要,你也可以通过编程方式添加 JWT 验证。
结果将是,如果JWT的iss声明不是[idp.example.com](https://idp.example.com),并且其aud声明列表中不包含[my-resource-server.example.com](https://my-resource-server.example.com),那么验证将失败。
覆盖或替换引导自动配置
Spring Boot 会代表资源服务器生成两个 @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 {
authorizeHttpRequests {
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 {
authorizeHttpRequests {
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,它将字符串令牌解码为经过验证的 Jwt 实例:
- Java
- Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
return JwtDecoders.fromIssuerLocation(issuerUri)
}
调用 JwtDecoders#fromIssuerLocation 会触发对 Provider Configuration 或 Authorization Server Metadata 端点的调用,以推导出 JWK Set URI。
如果应用程序没有暴露一个 JwtDecoder bean,那么 Spring Boot 将会暴露上述默认的 bean。
并且其配置可以通过 jwkSetUri() 方法进行覆盖,或使用 decoder() 方法进行替换。
或者,如果你完全没有使用Spring Boot,那么这两个组件——过滤器链和JwtDecoder——都可以在XML中进行配置。
过滤器链的配置方式如下:
- 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>
使用 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 {
authorizeHttpRequests {
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(),它将完全替换任何 Boot 对 JwtDecoder 的自动配置:
- 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 {
authorizeHttpRequests {
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>
暴露 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()
}
或者,您也可以使用颁发者,让 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)
}
配置受信任算法
默认情况下,NimbusJwtDecoder 以及资源服务器将仅信任并使用 RS256 算法验证令牌。
您可以通过 Spring Boot、NimbusJwtDecoder 构建器 或 JWK Set 响应 来自定义此配置。
通过 Spring Boot
设置算法的最简单方式是将其作为属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用构建器
然而,为了获得更强大的功能,我们可以使用 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 集响应
由于 Spring Security 的 JWT 支持基于 Nimbus,您同样可以利用其所有强大功能。
例如,Nimbus 提供了一个 JWSKeySelector 实现,它会根据 JWK Set 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)
}
信任单个非对称密钥
相比通过JWK Set端点支持资源服务器,更简单的方法是硬编码一个RSA公钥。该公钥可以通过Spring Boot或使用构建器来提供。
通过 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
使用构建器
要直接配置 RSAPublicKey,你可以简单地使用相应的 NimbusJwtDecoder 构建器,如下所示:
- Java
- Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withPublicKey(this.key).build()
}
信任单个对称密钥
使用单个对称密钥也很简单。你可以直接加载你的 SecretKey,并使用相应的 NimbusJwtDecoder 构建器,如下所示:
- Java
- Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withSecretKey(key).build()
}
配置授权
由 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((oauth2) -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
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> { }
手动提取权限
然而,在某些情况下,这种默认处理方式是不够的。例如,有些授权服务器并不使用 scope 属性,而是使用它们自定义的属性。或者,在其他时候,资源服务器可能需要将某个属性或一组属性适配为内部化的权限。
为此,Spring Security 提供了 JwtAuthenticationConverter,它负责将 JWT 转换为 Authentication。默认情况下,Spring Security 会为 JwtAuthenticationProvider 装配一个默认的 JwtAuthenticationConverter 实例。
在配置 JwtAuthenticationConverter 时,你可以提供一个辅助转换器,用于将 Jwt 转换为已授权权限的 Collection。
假设你的授权服务器通过一个名为 authorities 的自定义声明来传递权限。在这种情况下,你可以配置 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_,具体操作如下:
- 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 {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = CustomAuthenticationConverter()
}
}
}
return http.build()
}
}
配置验证
使用最小化Spring Boot配置,指定授权服务器的签发者URI后,资源服务器将默认验证iss声明以及exp和nbf时间戳声明。
在需要自定义验证的场景下,资源服务器内置了两个标准验证器,同时也支持自定义的 OAuth2TokenValidator 实例。
自定义时间戳验证
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
}
默认情况下,Resource Server 配置了 60 秒的时钟偏移量。
配置 RFC 9068 验证
如果你需要符合 RFC 9068 规范的令牌,可以通过以下方式配置验证:
- Java
- Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build();
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build());
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
.validateTypes(false).build()
jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
.audience("https://audience.example.org")
.clientId("client-identifier")
.issuer("https://issuer.example.org").build())
return jwtDecoder
}
配置自定义验证器
使用 OAuth2TokenValidator API 为 aud 声明 添加检查非常简单:
- 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 验证。
配置声明集映射
Spring Security 使用 Nimbus 库来解析 JWT 并验证其签名。因此,Spring Security 遵循 Nimbus 对每个字段值的解释以及如何将其强制转换为 Java 类型。
例如,由于Nimbus保持对Java 7的兼容性,它不使用Instant来表示时间戳字段。
而且完全可以使用其他库进行JWT处理,这些库可能有自己的强制类型转换规则,需要相应调整。
或者,更简单地说,资源服务器可能出于特定领域的原因,想要在JWT中添加或移除声明。
为此,Resource Server 支持使用 MappedJwtClaimSetConverter 来映射 JWT 声明集。
自定义单个声明的转换
默认情况下,MappedJwtClaimSetConverter 会尝试将声明转换为以下类型:
| 声明 | Java 类型 |
|---|---|
aud | Collection<String> |
exp | Instant |
iat | Instant |
iss | String |
jti | String |
nbf | Instant |
sub | String |
单个声明的转换策略可以通过 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 的默认声明转换器。
添加声明
MappedJwtClaimSetConverter 也可用于添加自定义声明,例如,以适应现有系统:
- Java
- Kotlin
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))
移除声明
移除声明也同样简单,使用相同的API即可:
- Java
- Kotlin
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))
重命名声明
在更复杂的场景中,例如同时查询多个声明或重命名声明时,资源服务器接受任何实现了 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 的构造函数。