对于最新的稳定版本,请使用 Spring Security 6.5.3! |
OAuth 2.0 资源服务器 JWT
JWT 的最小依赖关系
大多数资源服务器支持都收集到spring-security-oauth2-resource-server
.
但是,对解码和验证 JWT 的支持是spring-security-oauth2-jose
,这意味着为了拥有支持 JWT 编码的持有者Tokens的工作资源服务器,两者都是必要的。
JWT 的最小配置
使用 Spring Boot 时,将应用程序配置为资源服务器包括两个基本步骤。 首先,包括所需的依赖项,其次,指示授权服务器的位置。
指定授权服务器
在 Spring Boot 应用程序中,要指定要使用的授权服务器,只需执行以下作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
哪里idp.example.com/issuer
是iss
授权服务器将颁发的 JWT Tokens的声明。
资源服务器将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。
要使用issuer-uri 属性,也必须是idp.example.com/issuer/.well-known/openid-configuration ,idp.example.com/.well-known/openid-configuration/issuer 或idp.example.com/.well-known/oauth-authorization-server/issuer 是授权服务器支持的端点。
此终结点称为提供程序配置终结点或授权服务器元数据终结点。 |
就是这样!
创业期望
使用此属性和这些依赖项时,资源服务器将自动配置自身以验证 JWT 编码的持有者Tokens。
它通过确定性启动过程实现这一点:
-
点击提供程序配置或授权服务器元数据端点,处理
jwks_url
属性 -
配置验证策略以查询
jwks_url
对于有效的公钥 -
配置验证策略以验证每个 JWT
iss
索赔idp.example.com
.
此过程的结果是授权服务器必须启动并接收请求,资源服务器才能成功启动。
如果授权服务器在资源服务器查询时关闭(给定适当的超时),则启动将失败。 |
运行时预期
应用程序启动后,资源服务器将尝试处理包含Authorization: Bearer
页眉:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示此方案,资源服务器就会尝试根据持有者Tokens规范处理请求。
给定格式正确的 JWT,资源服务器将:
-
根据从
jwks_url
端点,并与 JWT 标头匹配 -
验证 JWT
exp
和nbf
时间戳和 JWTiss
claim 和 -
将每个作用域映射到具有前缀
SCOPE_
.
当授权服务器提供新密钥时,Spring Security将自动轮换用于验证JWTTokens的密钥。 |
由此产生的Authentication#getPrincipal
,默认情况下是 Spring SecurityJwt
对象,以及Authentication#getName
映射到 JWT 的sub
属性,如果存在的话。
从这里,考虑跳转到:
直接指定授权服务器 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
以便资源服务器仍会验证iss
对传入 JWT 的声明。
也可以直接在 DSL 上提供此属性。 |
覆盖或替换引导自动配置
有两个@Bean
Spring Boot 代表资源服务器生成的。
第一个是SecurityWebFilterChain
将应用配置为资源服务器。当包括spring-security-oauth2-jose
这SecurityWebFilterChain
看来:
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
如果应用程序未公开SecurityWebFilterChain
bean,那么 Spring Boot 将公开上述默认的。
替换它就像在应用程序中公开 bean 一样简单:
-
Java
-
Kotlin
@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();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/message/**", hasAuthority("SCOPE_message:read"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
以上要求范围message:read
对于任何以/messages/
.
方法oauth2ResourceServer
DSL 还将覆盖或替换自动配置。
例如,第二个@Bean
Spring Boot 创建的是一个ReactiveJwtDecoder
,解码String
Tokens转换为Jwt
:
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
叫ReactiveJwtDecoders#fromIssuerLocation 调用提供程序配置或授权服务器元数据端点以派生 JWK 集 URI。
如果应用程序未公开ReactiveJwtDecoder bean,那么 Spring Boot 将公开上述默认的。 |
并且可以使用jwkSetUri()
或使用decoder()
.
用jwkSetUri()
授权服务器的 JWK Set Uri 可以配置为配置属性,也可以在 DSL 中提供:
-
Java
-
Kotlin
@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();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
}
用jwkSetUri()
优先于任何配置属性。
用decoder()
比jwkSetUri()
是decoder()
,这将完全替换JwtDecoder
:
-
Java
-
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
}
当需要更深入的配置(如验证)时,这很方便。
公开ReactiveJwtDecoder
@Bean
或者,公开ReactiveJwtDecoder
@Bean
具有与decoder()
:
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
配置可信算法
默认情况下,NimbusReactiveJwtDecoder
,因此资源服务器将仅信任和验证使用RS256
.
您可以通过 Spring Boot 或 NimbusJwtDecoder 构建器对其进行自定义。
通过 Spring Boot
设置算法的最简单方法是作为属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用构建器
不过,为了获得更大的功能,我们可以使用附带的构建器NimbusReactiveJwtDecoder
:
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build()
}
叫jwsAlgorithm
将多次配置NimbusReactiveJwtDecoder
信任多个算法,如下所示:
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,您可以调用jwsAlgorithms
:
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
}
信任单个非对称密钥
比使用 JWK 集端点支持资源服务器更简单的方法是对 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: ConfigurableListableBeanFactory ->
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
直接,您可以简单地使用适当的NimbusReactiveJwtDecoder
builder,如下所示:
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withPublicKey(key).build()
}
信任单个对称密钥
使用单个对称键也很简单。
您只需加载您的SecretKey
并使用适当的NimbusReactiveJwtDecoder
builder,如下所示:
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
}
配置授权
从 OAuth 2.0 授权服务器颁发的 JWT 通常具有scope
或scp
属性,指示已授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些作用域强制到已授予权限的列表中,并在每个作用域前面加上字符串“SCOPE_”。
这意味着要保护作用域派生自 JWT 的端点或方法,相应的表达式应包含以下前缀:
-
Java
-
Kotlin
@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();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
authorize("/messages/**", hasAuthority("SCOPE_messages"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
或者类似的方法安全性:
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): Flux<Message> { }
手动提取权限
但是,在许多情况下,此默认值是不够的。例如,某些授权服务器不使用scope
属性,但具有自己的自定义属性。或者,在其他时候,资源服务器可能需要将属性或属性组合调整为内部化权限。
为此,DSL 公开了jwtAuthenticationConverter()
:
-
Java
-
Kotlin
@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);
}
@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
下面:
-
Java
-
Kotlin
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());
}
}
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>>
:
-
Java
-
Kotlin
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return Mono.just(jwt).map(this::doConversion);
}
}
internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
return Mono.just(jwt).map(this::doConversion)
}
}
配置验证
使用最少的 Spring Boot 配置,指示授权服务器的颁发者 uri,资源服务器将默认验证iss
claim 以及exp
和nbf
timestamp 声明。
在需要自定义验证的情况下,资源服务器附带两个标准验证器,并且还接受自定义OAuth2TokenValidator
实例。
自定义时间戳验证
JWT 通常有一个有效窗口,窗口的开头在nbf
claim 和exp
索赔。
但是,每台服务器都可能遇到时钟漂移,这可能导致Tokens在一台服务器上显示为过期,但对另一台服务器则不然。 随着分布式系统中协作服务器数量的增加,这可能会导致一些实现胃灼热。
资源服务器使用JwtTimestampValidator
以验证Tokens的有效性窗口,并且可以将其配置为clockSkew
为了缓解上述问题:
-
Java
-
Kotlin
@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;
}
@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
}
默认情况下,资源服务器将时钟偏差配置为 60 秒。 |
配置自定义验证器
为aud
声明很简单,使用OAuth2TokenValidator
应用程序接口:
-
Java
-
Kotlin
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);
}
}
}
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
实例:
-
Java
-
Kotlin
@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;
}
@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
}