|
对于最新的稳定版本,请使用 Spring Security 7.0.4! |
OAuth 2.0 资源服务器 JWT
JWT 的最小依赖
大部分资源服务器(Resource Server)的支持功能都集中在 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 Tokens中 iss 声明所包含的值。
资源服务器将使用此属性进行进一步的自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。
就这样了!
启动预期
当使用此属性和这些依赖项时,资源服务器将自动配置自身以验证 JWT 编码的 Bearer Token。
它通过在第一个包含JWT的请求中启动的一个确定性的发现过程来实现这一点:
-
查询 Provider Configuration 或 Authorization Server Metadata 端点以获取
jwks_url属性 -
查询
jwks_url端点以获取支持的算法 -
配置验证策略,以查询
jwks_url获取所发现算法的有效公钥 -
配置验证策略,以针对
iss验证每个 JWT 的idp.example.com声明。
推迟此过程的一个好处是,资源服务器的启动不再依赖于授权服务器的可用性。
|
此延迟由 |
运行时期望
一旦应用程序启动,资源服务器将尝试处理任何包含 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_的权限。
| 随着授权服务器提供新的密钥,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 中是如何工作的。
该图详细解释了来自 读取 Bearer Token 的图中 AuthenticationManager 的工作原理。
JwtAuthenticationProvider 的使用
身份验证Filter来自读取 Bearer Tokens传递一个BearerTokenAuthenticationToken 至 AuthenticationManager由以下实现ProviderManager.
这ProviderManager已配置为使用认证提供者类型JwtAuthenticationProvider.
JwtAuthenticationProvider解码、验证并校验Jwt使用一个JwtDecoder.
JwtAuthenticationProvider然后使用JwtAuthenticationConverter转换Jwt集成到Collection已授予权限。
当身份验证成功时,Authentication返回的内容类型为JwtAuthenticationToken并且拥有一个作为Jwt由配置返回JwtDecoder.
最终,返回的JwtAuthenticationToken将应用于SecurityContextHolder通过身份验证Filter.
直接指定授权服务器的 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 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
如果需要,您也可以以编程方式添加 对 aud 的验证。 |
结果将是:如果 JWT 的 iss 声明不是 idp.example.com,并且其 aud 声明的列表中不包含 my-resource-server.example.com,则验证将失败。
覆盖或替换 Boot 自动配置
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 {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}
如果应用程序未暴露一个 SecurityFilterChain bean,那么 Spring Boot 将会暴露上述默认的 bean。
只需在应用程序中暴露该 Bean 即可完成替换:
-
Java
-
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;
@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/messages/**").access(hasScope("message:read"))
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.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()
}
}
上述配置要求任何以 message:read 开头的 URL 都必须具有 /messages/ 范围。
oauth2ResourceServer DSL 中的方法也会覆盖或替换自动配置。
例如,Spring Boot 创建的第二个 @Bean 是 JwtDecoder,它 将 String 个Tokens解码为 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 {
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(),它将完全替换任何 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 {
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>
公开一个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 时让 jwkSetUri 自动查找 build(),如下所示:
-
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 算法验证Tokens。
您可以通过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 提供的所有强大功能。
例如,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 提供,也可以通过 使用构建器(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.location: hfds://my-key.pub
然后自动装配该值:
-
Java
-
Kotlin
@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null
信任单一对称密钥
使用单一的对称密钥也很简单。
你只需加载你的 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 属性,用于指明该Tokens已被授予的作用域(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器(Resource Server)会尝试将这些作用域(scopes)转换为一组授予的权限(granted authorities),并在每个作用域前加上字符串“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 {
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> { }
手动提取权限
然而,在许多情况下,这种默认设置是不够的。
例如,某些授权服务器不使用 scope 属性,而是使用它们自己的自定义属性。
或者,在其他时候,资源服务器可能需要将该属性或多个属性的组合转换为内部的权限(authorities)。
为此,Spring Security 提供了 JwtAuthenticationConverter,它负责将 Jwt 转换为 Authentication。
默认情况下,Spring Security 会将 JwtAuthenticationProvider 与 JwtAuthenticationConverter 的默认实例进行连接。
作为配置 JwtAuthenticationConverter 的一部分,您可以提供一个辅助转换器,用于将 Jwt 转换为一组授予的权限(Collection of granted authorities)。
假设您的授权服务器通过名为 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 {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = CustomAuthenticationConverter()
}
}
}
return http.build()
}
}
配置验证
使用最小化的 Spring Boot 配置,指定授权服务器的 issuer URI 后,资源服务器将默认验证 iss 声明以及 exp 和 nbf 时间戳声明。
在需要自定义验证的情况下,资源服务器(Resource Server)内置了两个标准验证器,同时也接受自定义的 OAuth2TokenValidator 实例。
自定义时间戳验证
JWT 通常具有一个有效时间窗口,该窗口的起始时间由 nbf 声明指示,结束时间由 exp 声明指示。
然而,每台服务器都可能出现时钟漂移,这会导致Tokens在一台服务器上看似已过期,而在另一台服务器上却未过期。 随着分布式系统中协作服务器数量的增加,这种情况可能会给某些实现带来困扰。
资源服务器使用 JwtTimestampValidator 来验证Tokens的有效时间窗口,并可通过配置 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秒的时钟偏移。 |
配置 RFC 9068 验证
如果你需要要求Tokens符合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 添加或移除声明。
为此,资源服务器支持使用 MappedJwtClaimSetConverter 映射 JWT 声明集。
自定义单个声明的转换
默认情况下,MappedJwtClaimSetConverter 会尝试将声明(claims)强制转换为以下类型:
声明 |
Java 类型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可以使用 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 }))
重命名声明
在更复杂的场景中,例如同时查询多个声明(claims)或重命名某个声明时,资源服务器(Resource Server)接受任何实现了 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秒。
在某些场景下,这可能太短了。 此外,它也没有考虑更复杂的模式,例如退避(back-off)和发现(discovery)。
要调整资源服务器连接授权服务器的方式,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。 |