|
此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.3! |
OAuth 2.0 资源服务器 JWT
JWT 的最小依赖项
大多数 Resource Server 支持都收集到spring-security-oauth2-resource-server.
但是,对解码和验证 JWT 的支持在spring-security-oauth2-jose,这意味着两者都是必要的,以便拥有支持 JWT 编码的 Bearer Token 的工作资源服务器。
JWT 的最低配置
使用 Spring Boot 时,将应用程序配置为资源服务器包括两个基本步骤。 首先,包括所需的依赖项,其次,指示授权服务器的位置。
指定 Authorization Server
在 Spring Boot 应用程序中,要指定要使用的授权服务器,只需执行以下作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
哪里idp.example.com/issuer是issAuthorization Server 将颁发的 JWT 令牌的声明。
Resource Server 将使用此属性进一步进行自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。
要使用issuer-uriproperty 中,还必须是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是授权服务器支持的端点。
此端点称为 Provider Configuration 端点或 Authorization Server Metadata 端点。 |
就是这样!
创业期望
使用此属性和这些依赖项时,Resource Server 将自动配置自身以验证 JWT 编码的不记名令牌。
它通过确定性启动过程来实现这一点:
-
查询 Provider Configuration 或 Authorization Server Metadata 端点以获取
jwks_url财产 -
查询
jwks_url终端节点 -
配置验证策略以查询
jwks_url对于找到的算法的有效公钥 -
配置验证策略以验证每个 JWT
iss索赔idp.example.com.
此过程的结果是,授权服务器必须启动并接收请求,Resource Server 才能成功启动。
| 如果 Resource Server 查询授权服务器时授权服务器已关闭(给定适当的超时),则启动将失败。 |
运行时预期
应用程序启动后,Resource Server 将尝试处理任何包含Authorization: Bearer页眉:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指定了此方案,Resource Server 就会尝试根据 Bearer Token 规范处理请求。
给定格式正确的 JWT,Resource Server 将:
-
根据从
jwks_urlendpoint 并与 JWT 匹配 -
验证 JWT 的
exp和nbftimestamp 和 JWT 的issclaim 和 -
将每个范围映射到带有前缀的颁发机构
SCOPE_.
| 当授权服务器提供新密钥时, Spring Security 将自动轮换用于验证 JWT 的密钥。 |
结果Authentication#getPrincipal默认情况下,是 Spring SecurityJwtobject 和Authentication#getName映射到 JWT 的sub属性(如果存在)。
从这里,考虑跳到:
JWT 身份验证的工作原理
接下来,让我们看看 Spring Security 用于在基于 servlet 的应用程序中支持 JWT 身份验证的架构组件,就像我们刚刚看到的一样。
让我们来看看JwtAuthenticationProvider在 Spring Security 中工作。
该图详细介绍了AuthenticationManager在 Reading the Bearer Token works 中的数字中。
JwtAuthenticationProvider用法
身份验证Filter从 Reading the Bearer Token 传递一个BearerTokenAuthenticationToken到AuthenticationManager它由ProviderManager.
这ProviderManager配置为使用 AuthenticationProvider 类型的JwtAuthenticationProvider.
JwtAuthenticationProvider解码、验证和验证Jwt使用JwtDecoder.
JwtAuthenticationProvider然后使用JwtAuthenticationConverter要将Jwt转换为Collection的授予权限。
身份验证成功后,Authentication返回的 类型为JwtAuthenticationToken并且有一个主体,该主体是Jwt由配置的JwtDecoder.
最终,返回的JwtAuthenticationToken将在SecurityContextHolder通过身份验证Filter.
直接指定授权服务器 JWK 集 URI
如果授权服务器不支持任何配置端点,或者如果 Resource Server 必须能够独立于授权服务器启动,则jwk-set-uri也可以提供:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
| JWK 集 URI 未标准化,但通常可以在授权服务器的文档中找到 |
因此,Resource Server 不会在启动时 ping 授权服务器。
我们仍然指定issuer-uri,以便 Resource Server 仍会验证iss对传入的 JWT 的声明。
| 此属性也可以直接在 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及其audclaim 不包含my-resource-server.example.com,则验证将失败。
覆盖或替换引导自动配置
有两个@Bean的 Spring Boot 代表 Resource Server 生成的。
第一个是SecurityFilterChain,将应用程序配置为资源服务器。当包含spring-security-oauth2-jose这SecurityFilterChain看来:
-
Java
-
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
return http.build()
}
如果应用程序没有公开SecurityFilterChainbean,则 Spring Boot 将公开上述默认的 bean。
替换它就像在应用程序中公开 bean 一样简单:
-
Java
-
Kotlin
@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();
}
}
@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()
}
}
以上要求message:read对于任何以/messages/.
方法oauth2ResourceServerDSL 还将覆盖或替换 auto 配置。
例如,第二个@BeanSpring Boot 创建的是一个JwtDecoder哪解码String令牌转换为已验证的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 的内容。 |
如果应用程序没有公开JwtDecoderbean,则 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 集 URI 可以配置为配置属性,也可以在 DSL 中提供:
-
Java
-
Kotlin
-
Xml
@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();
}
}
@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:
-
Java
-
Kotlin
-
Xml
@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();
}
}
@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():
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
配置可信算法
默认情况下,NimbusJwtDecoder,因此 Resource Server 将仅使用RS256.
您可以通过 Spring Boot、NimbusJwtDecoder 构建器或 JWK Set 响应对其进行自定义。
通过 Spring Boot
设置算法的最简单方法是 as a property:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用 Builder
不过,为了获得更大的功能,我们可以使用附带NimbusJwtDecoder:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build()
}
叫jwsAlgorithm将配置NimbusJwtDecoder信任多个算法,如下所示:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,您可以调用jwsAlgorithms:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@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 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 端点支持 Resource Server 更简单的方法是对 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并使用适当的NimbusJwtDecoderbuilder 中,如下所示:
-
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"}
在这种情况下,Resource Server 将尝试将这些范围强制转换为已授予的权限列表,并在每个范围前加上字符串“SCOPE_”。
这意味着,要保护具有从 JWT 派生的作用域的端点或方法,相应的表达式应包含以下前缀:
-
Java
-
Kotlin
-
Xml
@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();
}
}
@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>
或者与方法安全性类似:
-
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);
}
}
// ...
@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)
}
}
// ...
@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,Resource Server 将默认验证issclaim 以及exp和nbftimestamp 声明。
在需要自定义验证的情况下,Resource Server 附带了两个标准验证程序,并且还接受自定义OAuth2TokenValidator实例。
自定义时间戳验证
JWT 通常有一个有效期窗口,窗口的开始在nbfclaim 和exp索赔。
但是,每个服务器都可能遇到 clock drift,这可能导致令牌对一个服务器显示已过期,但对另一个服务器则不然。 随着分布式系统中协作服务器数量的增加,这可能会导致一些实施 Burn。
Resource Server 使用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 秒。 |
配置自定义验证器
添加检查这aud索赔很简单,使用OAuth2TokenValidator应用程序接口:
-
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
}
如前所述,您可以改为配置audBoot 中的 validation. |
配置声明集映射
Spring Security 使用 Nimbus 库来解析 JWT 并验证其签名。 因此, Spring Security 受制于 Nimbus 对每个字段值的解释以及如何将每个字段值强制转换为 Java 类型。
例如,由于 Nimbus 仍然与 Java 7 兼容,因此它不会使用Instant来表示时间戳字段。
并且完全可以使用不同的库或进行 JWT 处理,这可能会做出自己的强制决定,需要调整。
或者,很简单,资源服务器可能出于特定于域的原因希望在 JWT 中添加或删除声明。
出于这些目的,Resource Server 支持将 JWT 声明集与MappedJwtClaimSetConverter.
自定义单个索赔的转换
默认情况下,MappedJwtClaimSetConverter将尝试将声明强制转换为以下类型:
索赔 |
Java 类型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
单个索赔的转换策略可以使用MappedJwtClaimSetConverter.withDefaults:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
jwtDecoder.setClaimSetConverter(converter);
return jwtDecoder;
}
@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
-
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 }))
重命名索赔
在更复杂的场景中,例如一次咨询多个声明或重命名声明,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.withJwkSetUri(jwkSetUri).build();
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
return jwtDecoder
}
配置超时
默认情况下,Resource Server 使用各 30 秒的连接和套接字超时来与授权服务器进行协调。
在某些情况下,这可能太短了。 此外,它没有考虑更复杂的模式,如 back-off 和 discovery。
要调整 Resource Server 与授权服务器的连接方式,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.withJwkSetUri(jwkSetUri).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.withJwkSetUri(jwkSetUri).restOperations(rest).build()
}
此外,默认情况下,Resource Server 在内存中将授权服务器的 JWK 集缓存 5 分钟,您可能需要调整该时间。 此外,它没有考虑更复杂的缓存模式,例如驱逐或使用共享缓存。
要调整 Resource Server 缓存 JWK 集的方式,NimbusJwtDecoder接受Cache:
-
Java
-
Kotlin
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build();
}
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.cache(cacheManager.getCache("jwks"))
.build()
}
当给定Cache,Resource Server 将使用 JWK 集 URI 作为键,使用 JWK 集 JSON 作为值。
Spring 不是缓存提供程序,因此您需要确保包含适当的依赖项,例如spring-boot-starter-cache以及您最喜欢的缓存提供商。 |
无论是套接字超时还是缓存超时,你可能都希望直接与 Nimbus 一起使用。
为此,请记住NimbusJwtDecoder附带一个采用 Nimbus 的JWTProcessor. |