此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.1! |
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
是iss
Authorization Server 颁发的 JWT 令牌的声明。
此资源服务器使用此属性进一步进行自我配置、发现授权服务器的公钥,并随后验证传入的 JWT。
要使用 |
创业期望
使用此属性和这些依赖项时,Resource Server 会自动将自身配置为验证 JWT 编码的 Bearer Tokens。
它通过确定性启动过程来实现这一点:
-
点击 Provider Configuration 或 Authorization Server Metadata 端点,处理
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_url
endpoint 的 intent 匹配,并与 JWTs 标头匹配。 -
验证 JWT
exp
和nbf
timestamp 和 JWTiss
索赔。 -
将每个范围映射到带有前缀
SCOPE_
.
当授权服务器提供新密钥时, Spring Security 会自动轮换用于验证 JWT 令牌的密钥。 |
默认情况下,生成的Authentication#getPrincipal
是 Spring SecurityJwt
object 和Authentication#getName
映射到 JWT 的sub
属性(如果存在)。
从这里,考虑跳到:
直接指定授权服务器 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 上提供此属性。 |
覆盖或替换引导自动配置
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
在应用程序中:
-
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
对于任何以/messages/
.
方法oauth2ResourceServer
DSL 还会覆盖或替换 auto 配置。
例如,第二个@Bean
Spring Boot 创建的是一个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 集 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()
,因为它完全替换了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
,因此 Resource Server 仅信任和验证使用RS256
.
您可以使用 Spring Boot 或使用 NimbusJwtDecoder 构建器来自定义此行为。
使用 Spring Boot 自定义可信算法
设置算法的最简单方法是 as a property:
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 端点支持 Resource Server 更简单的方法是对 RSA 公钥进行硬编码。 公钥可以通过 Spring Boot 或 Using a 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
信任单个对称密钥
您也可以使用单个对称密钥。
您可以在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 { }
}
}
}
你可以对 method security 做类似的事情:
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): Flux<Message> { }
手动提取权限
但是,在许多情况下,这种违约是不够的。
例如,某些授权服务器不使用scope
属性。相反,它们有自己的 custom 属性。
在其他时候,资源服务器可能需要将属性或属性组合调整为内部化权限。
为此,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,Resource Server 默认验证iss
claim 以及exp
和nbf
timestamp 声明。
在需要自定义验证需求的情况下,Resource Server 附带两个标准验证程序,并且还接受自定义OAuth2TokenValidator
实例。
自定义时间戳验证
JWT 实例通常有一个有效期窗口,窗口的开始在nbf
claim 和exp
索赔。
但是,每个服务器都可能遇到 clock drift,这可能导致令牌对一个服务器似乎已过期,而对另一个服务器则未过期。 这可能会导致一些实施 Hotburn,因为分布式系统中协作服务器的数量会增加。
Resource Server 使用JwtTimestampValidator
验证令牌的有效性窗口,您可以使用clockSkew
为了缓解 clock drift 问题:
-
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
}
默认情况下,Resource Server 配置的时钟偏差为 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
}