# OAuth2.0 资源服务器 JWT
# JWT 的最小依赖项
大多数资源服务器支持被收集到spring-security-oauth2-resource-server
中。然而,对 JWTS 的解码和验证的支持是spring-security-oauth2-jose
,这意味着这两个都是必要的,以便拥有一个支持 JWT 编码的承载令牌的工作资源服务器。
# JWTS 的最小配置
当使用Spring Boot (opens new window)时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。
# 指定授权服务器
在 Spring 引导应用程序中,要指定使用哪个授权服务器,只需执行以下操作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
其中[https://idp.example.com/issuer](https://idp.example.com/issuer)
是授权服务器将发布的 JWT 令牌的iss
声明中包含的值。Resource Server 将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的 JWTS。
要使用issuer-uri 属性,还必须证明[https://idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration) 、[https://idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer) 或[https://idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer) 中的一个是授权服务器所支持的端点。此端点被称为提供者配置 (opens new window)端点或授权服务器元数据 (opens new window)端点。 |
---|
就这样!
# 创业期望
当使用此属性和这些依赖项时,Resource Server 将自动配置自身以验证 JWT 编码的承载令牌。
它通过一个确定性的启动过程来实现这一点:
点击提供者配置或授权服务器元数据端点,处理
jwks_url
属性的响应将验证策略配置为查询
jwks_url
中的有效公钥将验证策略配置为针对
[https://idp.example.com](https://idp.example.com)
的每个 JWTSiss
索赔进行验证。
此过程的结果是,为了使资源服务器成功启动,授权服务器必须启动并接收请求。
如果在资源服务器查询时,授权服务器处于关闭状态(给定适当的超时),那么启动将失败。 |
---|
# 运行时期望
一旦启动应用程序,Resource Server 将尝试处理任何包含Authorization: Bearer
报头的请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要表明了该方案,资源服务器就会尝试根据承载令牌规范来处理请求。
给定一个格式良好的 JWT,资源服务器将:
根据在启动过程中从
jwks_url
端点获得并与 JWTS 头匹配的公钥验证其签名验证 JWTS
exp
和nbf
时间戳和 JWTSiss
声明,并将每个作用域映射到一个前缀
SCOPE_
的权限。
由于授权服务器提供了可用的新密钥, Spring 安全性将自动旋转用于验证 JWT 令牌的密钥。 |
---|
默认情况下,生成的Authentication#getPrincipal
是 Spring securityJwt
对象,并且Authentication#getName
映射到 JWT 的sub
属性(如果存在)。
从这里开始,考虑跳到:
如何在不将资源服务器启动与授权服务器的可用性绑定的情况下进行配置
How to Configure without Spring Boot
# 直接指定授权服务器 JWK 设置的 URI
如果授权服务器不支持任何配置端点,或者如果资源服务器必须能够独立于授权服务器启动,那么也可以提供jwk-set-uri
:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
JWK 集 URI 不是标准化的,但通常可以在授权服务器的文档中找到。 |
---|
因此,资源服务器不会在启动时对授权服务器进行 ping。我们仍然指定issuer-uri
,以便 Resource Server 仍然在传入的 JWTS 上验证iss
声明。
也可以在DSL上直接提供此属性。 |
---|
# 覆盖或替换 Boot Auto 配置
有两个@Bean
s, Spring boot 代表资源服务器生成。
第一个是将应用程序配置为资源服务器的SecurityWebFilterChain
。当包含spring-security-oauth2-jose
时,这个SecurityWebFilterChain
看起来是这样的:
例 1。资源服务器 SecurityWebFilterchain
爪哇
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
Kotlin
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
如果应用程序不公开SecurityWebFilterChain
Bean,那么 Spring 引导将公开上面的默认引导。
替换它就像在应用程序中公开 Bean 一样简单:
例 2。替换 SecurityWebFilterchain
爪哇
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(withDefaults())
);
return http.build();
}
Kotlin
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/message/**", hasAuthority("SCOPE_message:read"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
对于任何以/messages/
开头的 URL,上述条件要求message:read
的范围。
oauth2ResourceServer
DSL 上的方法也将覆盖或替换自动配置。
例如,第二个@Bean
Spring 引导创建了一个ReactiveJwtDecoder
,它将String
令牌解码为Jwt
的验证实例:
例 3。ReactiveJWTdecoder
爪哇
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
调用[ReactiveJwtDecoders#fromIssuerLocation](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-) 是调用提供者配置或授权服务器元数据端点的目的,以便派生 JWK 集 URI。如果应用程序不公开 ReactiveJwtDecoder Bean,那么 Spring 引导将公开上述默认的。 |
---|
并且其配置可以使用jwkSetUri()
重写或使用decoder()
替换。
# `
可以配置授权服务器的 JWK 集 URI作为配置属性,也可以在 DSL 中提供它:
爪哇
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
return http.build();
}
Kotlin
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
}
使用jwkSetUri()
优先于任何配置属性。
# `
比jwkSetUri()
更强大的是decoder()
,它将完全取代JwtDecoder
的任何引导自动配置:
爪哇
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
return http.build();
}
Kotlin
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
}
当需要更深的配置时,比如validation,这是很方便的。
# 曝光ReactiveJwtDecoder``@Bean
或者,暴露ReactiveJwtDecoder``@Bean
具有与decoder()
相同的效果:
爪哇
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
# 配置可信算法
默认情况下,NimbusReactiveJwtDecoder
以及资源服务器将仅使用RS256
信任和验证令牌。
你可以通过Spring Boot或NimbusJWTDecoder Builder对此进行自定义。
# 通过 Spring 引导
设置算法的最简单方法是作为一个属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
# 使用构建器
不过,为了获得更大的功率,我们可以使用带有NimbusReactiveJwtDecoder
的建造器:
爪哇
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build()
}
多次调用jwsAlgorithm
将配置NimbusReactiveJwtDecoder
来信任多个算法,如下所示:
爪哇
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,你可以调用jwsAlgorithms
:
爪哇
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
}
# 信任单一的非对称密钥
比支持具有 JWK 设置端点的资源服务器更简单的方法是对 RSA 公钥进行硬编码。公钥可以通过Spring Boot或使用构建器提供。
# 通过 Spring 引导
通过 Spring 引导指定一个键非常简单。密钥的位置可以这样指定:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
或者,为了允许更复杂的查找,你可以对RsaKeyConversionServicePostProcessor
进行后处理:
例 4。BeanFactoryPostprocessor
爪哇
@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
Kotlin
@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
return BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory ->
beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
.setResourceLoader(CustomResourceLoader())
}
}
指定你的密钥的位置:
key.location: hfds://my-key.pub
然后自动连接该值:
爪哇
@Value("${key.location}")
RSAPublicKey key;
Kotlin
@Value("\${key.location}")
val key: RSAPublicKey? = null
# 使用构建器
要直接连接RSAPublicKey
,只需使用适当的NimbusReactiveJwtDecoder
构建器,如下所示:
爪哇
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withPublicKey(key).build()
}
# 信任单一的对称密钥
使用单一的对称密钥也很简单。你可以简单地在SecretKey
中加载,并使用适当的NimbusReactiveJwtDecoder
构建器,如下所示:
爪哇
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
}
# 配置授权
从 OAuth2.0 授权服务器发出的 JWT 通常具有scope
或scp
属性,指示已授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,Resource Server 将尝试强制将这些作用域放入一个已授予权限的列表中,并在每个作用域前加上字符串“scope_”。
这意味着,要使用从 JWT 派生的作用域来保护端点或方法,相应的表达式应该包括以下前缀:
爪哇
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
return http.build();
}
Kotlin
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
authorize("/messages/**", hasAuthority("SCOPE_messages"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
或类似于方法安全性:
爪哇
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): Flux<Message> { }
# 手动提取权限
然而,在许多情况下,这种默认设置是不够的。例如,一些授权服务器不使用scope
属性,而是具有自己的自定义属性。或者,在其他时候,资源服务器可能需要将属性或属性的组合调整为内在化的权限。
为此,DSL 公开jwtAuthenticationConverter()
:
爪哇
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
)
);
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
Kotlin
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = grantedAuthoritiesExtractor()
}
}
}
}
fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> {
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor())
return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
}
它负责将Jwt
转换为Authentication
。作为其配置的一部分,我们可以提供一个从Jwt
到Collection
的辅助转换器。
最后的转换器可能类似于下面的GrantedAuthoritiesExtractor
:
爪哇
static class GrantedAuthoritiesExtractor
implements Converter<Jwt, Collection<GrantedAuthority>> {
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<?> authorities = (Collection<?>)
jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList());
return authorities.stream()
.map(Object::toString)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
Kotlin
internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
val authorities: List<Any> = jwt.claims
.getOrDefault("mycustomclaim", emptyList<Any>()) as List<Any>
return authorities
.map { it.toString() }
.map { SimpleGrantedAuthority(it) }
}
}
为了具有更大的灵活性,DSL 支持用实现Converter<Jwt, Mono<AbstractAuthenticationToken>>
的任何类完全替换转换器:
爪哇
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return Mono.just(jwt).map(this::doConversion);
}
}
Kotlin
internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
return Mono.just(jwt).map(this::doConversion)
}
}
# 配置验证
使用minimal Spring Boot configuration(表示授权服务器的发行者 URI),资源服务器将默认验证iss
声明以及exp
和nbf
时间戳声明。
在需要定制验证的情况下,Resource Server 附带两个标准验证器,并且还接受定制的OAuth2TokenValidator
实例。
# 自定义时间戳验证
JWT 通常有一个有效窗口,窗口的开始在nbf
声明中指示,结束在exp
声明中指示。
然而,每个服务器都可能经历时钟漂移,这可能导致令牌在一台服务器上出现过期,而不是在另一台服务器上出现。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现令人心烦。
Resource Server 使用JwtTimestampValidator
来验证令牌的有效性窗口,并且可以将其配置为clockSkew
以缓解上述问题:
爪哇
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new IssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
JwtTimestampValidator(Duration.ofSeconds(60)),
JwtIssuerValidator(issuerUri))
jwtDecoder.setJwtValidator(withClockSkew)
return jwtDecoder
}
默认情况下,Resource Server 配置的时钟偏差为 60 秒。 |
---|
# 配置自定义验证器
使用OAuth2TokenValidator
API 添加aud
声明的检查很简单:
爪哇
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
Kotlin
class AudienceValidator : OAuth2TokenValidator<Jwt> {
var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null)
override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
return if (jwt.audience.contains("messaging")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(error)
}
}
}
然后,要添加到资源服务器中,需要指定ReactiveJwtDecoder
实例:
爪哇
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
Kotlin
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator()
val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}