|
此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4! |
OAuth 2.0 资源服务器 JWT
JWT 的最小依赖
大多数资源服务器的支持被收集在 spring-security-oauth2-resource-server 中。
然而,解码和验证 JWT 的支持在 spring-security-oauth2-jose 中,这意味着为了有一个支持 JWT 编码的 Bearer Tokens 的工作中的资源服务器,两者都是必要的。
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的请求中启动的一个确定性的发现过程来实现这一点:
-
调用提供者配置或授权服务器元数据端点,处理响应中的
属性。 -
配置验证策略以查询
jwks_url获取有效的公钥。 -
配置验证策略,对每个JWT的
iss声明进行验证,确保其与idp.example.com一致。
推迟此过程的一个好处是,资源服务器的启动不再依赖于授权服务器的可用性。
|
此延迟由 |
运行时期望
一旦应用程序启动后,资源服务器会尝试处理包含Authorization: Bearer头的任何请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指定了此方案,资源服务器就会根据Bearer Token规范来处理请求。
给定一个格式正确的JWT(JSON Web Token),资源服务器:
-
在启动时,验证其签名与从
jwks_url端点获取的公钥进行匹配,并与JWT头部匹配。 -
验证JWT的
exp和时间戳以及JWT的 iss声明。 -
将每个作用域映射到具有前缀
SCOPE_的权限。
|
当授权服务器提供新的密钥时,Spring Security 会自动轮换用于验证JWTTokens的密钥。 |
默认情况下,Authentication#getPrincipal 的结果是一个 Spring Security Jwt 对象,并且 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 集合 URI 没有标准化,但您通常可以在授权服务器的文档中找到它。 |
因此,资源服务器不会在启动时向授权服务器发送ping请求。
我们仍然指定issuer-uri,以便资源服务器仍然会验证传入的JWT中的iss声明。
|
您可以直接在DSL上提供此属性。 |
覆盖或替换 Boot 自动配置
Spring Boot为资源服务器生成两个@Bean对象。
The first bean is a SecurityWebFilterChain that configures the application as a resource server. When including spring-security-oauth2-jose, this SecurityWebFilterChain looks like:
-
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 颗粒度,Spring Boot 将暴露默认的一个(如前一个列表所示)。
要替换它,请在应用程序中公开@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 { }
}
}
}
该配置需要对任何以message:read开头的URL具有/messages/的作用域。
oauth2ResourceServer DSL上的方法也会覆盖或替换自动配置。
例如,Spring Boot 创建的第二个 @Bean 是一个 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 Set URI。
如果应用程序没有暴露 |
其配置可以通过使用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()
}
或者,你可以使用颁发者(issuer),并在调用 NimbusReactiveJwtDecoder 时让 jwkSetUri 自动查找 build(),如下所示:
-
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的Tokens。
您可以使用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"}
当这种情况发生时,Resource Server 尝试将这些范围强制转换为授权列表,并在每个范围前加上字符串 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时间戳声明。
在需要自定义验证需求的情况下,Resource Server 配备了两个标准验证器,并且也接受自定义的 OAuth2TokenValidator 实例。
自定义时间戳验证
JWT 实例通常具有一个有效窗口,在此期间 JWT 有效。该窗口的开始时间由 nbf 声明指示,结束时间由 exp 声明指示。
然而,每个服务器都可能出现时钟漂移的情况,这会导致Tokens在一台服务器上看起来已经过期而在另一台服务器上却不是这样。<br> 这种情况在分布式系统中协作的服务器数量增加时可能会引发一些实现上的烦恼。
资源服务器使用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秒的时钟偏移。 |
配置 RFC 9068 验证
如果你需要要求Tokens符合RFC 9068标准,可以按以下方式配置验证:
-
Java
-
Kotlin
@Bean
JwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.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 = NimbusReactiveJwtDecoder.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
}
配置自定义验证器
您可以使用aud API 添加对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
}