OAuth 2.0 资源服务器 JWT
JWT 的最小依赖项
大多数资源服务器支持都包含在 spring-security-oauth2-resource-server
中。但是,对解码和验证 JWT 的支持位于 spring-security-oauth2-jose
中,这意味着两者对于支持 JWT 编码的 Bearer 令牌的正常运行的资源服务器都是必要的。
JWT 的最小配置
当使用 Spring Boot 时,将应用程序配置为资源服务器包括两个基本步骤。首先,包含所需的依赖项。其次,指示授权服务器的位置。
指定授权服务器
在 Spring Boot 应用程序中,您需要指定要使用的授权服务器
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
其中 idp.example.com/issuer
是授权服务器发出的 JWT 令牌的 iss
声明中包含的值。此资源服务器使用此属性进行进一步的自配置,发现授权服务器的公钥,然后验证传入的 JWT。
要使用 |
启动预期
当使用此属性和这些依赖项时,资源服务器会自动配置自身以验证 JWT 编码的 Bearer 令牌。
它通过确定性的启动过程实现这一点
-
访问提供程序配置或授权服务器元数据端点,处理
jwks_url
属性的响应。 -
配置验证策略以查询
jwks_url
以获取有效的公钥。 -
配置验证策略以针对
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_
为前缀的权限。
随着授权服务器提供新的密钥,Spring Security会自动轮换用于验证JWT令牌的密钥。 |
默认情况下,生成的Authentication#getPrincipal
是一个Spring Security Jwt
对象,并且Authentication#getName
映射到JWT的sub
属性(如果存在)。
从这里,考虑跳转到
直接指定授权服务器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上直接提供此属性。 |
覆盖或替换Boot自动配置
Spring Boot代表资源服务器生成两个@Bean
对象。
第一个bean是一个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(如前面的列表所示)。
要替换它,请在应用程序中公开@Bean
-
Java
-
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/message/**").access(hasScope("message:read"))
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(withDefaults())
);
return http.build();
}
import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/message/**", hasScope("message:read"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
前面的配置要求任何以/messages/
开头的URL都具有message:read
的作用域。
oauth2ResourceServer
DSL上的方法也会覆盖或替换自动配置。
例如,Spring Boot创建的第二个@Bean
是ReactiveJwtDecoder
,它将String
令牌解码为经过验证的Jwt
实例。
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
调用 |
可以通过使用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()
decoder()
比jwkSetUri()
更强大,因为它完全替换了Spring Boot对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()
具有相同的效果:您可以使用jwkSetUri
构造一个,如下所示
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
或者,您可以使用发行者并让NimbusReactiveJwtDecoder
在调用build()
时查找jwkSetUri
,如下所示
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
}
或者,如果默认值适合您,您也可以使用JwtDecoders
,它除了配置解码器的验证器之外,还会执行上述操作。
-
Java
-
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuer)
}
配置受信任的算法
默认情况下,NimbusReactiveJwtDecoder
以及资源服务器仅信任和验证使用RS256
的令牌。
您可以使用Spring Boot或使用NimbusJwtDecoder构建器来自定义此行为。
使用Spring Boot自定义受信任的算法
设置算法的最简单方法是将其作为属性。
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用构建器自定义受信任的算法
但是,为了获得更大的功能,我们可以使用NimbusReactiveJwtDecoder
附带的构建器。
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
}
多次调用jwsAlgorithm
会将NimbusReactiveJwtDecoder
配置为信任多个算法。
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
或者,您可以调用jwsAlgorithms
。
-
Java
-
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
}
信任单个非对称密钥
比使用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: 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
信任单个对称密钥
您还可以使用单个对称密钥。您可以加载您的SecretKey
并使用相应的NimbusReactiveJwtDecoder
构建器。
-
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
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.mvcMatchers("/contacts/**").access(hasScope("contacts"))
.mvcMatchers("/messages/**").access(hasScope("messages"))
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
return http.build();
}
import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/contacts/**", hasScope("contacts"))
authorize("/messages/**", hasScope("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)
}
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
声明以及exp
和nbf
时间戳声明。
在您需要自定义验证需求的情况下,资源服务器附带两个标准验证器,并且还接受自定义的OAuth2TokenValidator
实例。
自定义时间戳验证
JWT实例通常具有有效期窗口,窗口的开始由nbf
声明指示,结束由exp
声明指示。
但是,每个服务器都可能遇到时钟漂移,这会导致令牌对一个服务器似乎已过期,但对另一个服务器则没有。随着分布式系统中协作服务器数量的增加,这可能会导致某些实现上的麻烦。
资源服务器使用JwtTimestampValidator
来验证令牌的有效期窗口,您可以使用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秒的时钟偏差。 |
配置自定义验证器
您可以使用OAuth2TokenValidator
API添加对aud
声明的检查。
-
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
}