跳到主要内容

OAuth 2.0 资源服务器 JWT

QWen Max 中英对照 JWT OAuth 2.0 Resource Server JWT

JWT 的最小依赖

大多数资源服务器的支持都集中在 spring-security-oauth2-resource-server 中。然而,解码和验证 JWT 的支持在 spring-security-oauth2-jose 中,这意味着两者都是必需的,才能使支持 JWT 编码的承载令牌的资源服务器正常工作。

JWT的最小配置

当使用 Spring Boot 时,将应用程序配置为资源服务器包括两个基本步骤。首先,包含所需的依赖项。其次,指定授权服务器的位置。

指定授权服务器

在Spring Boot应用程序中,你需要指定使用哪个授权服务器:

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
yml

其中 [idp.example.com/issuer](https://idp.example.com/issuer) 是授权服务器颁发的 JWT 令牌中 iss 声明所包含的值。该资源服务器使用此属性进行进一步的自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。

备注

要使用 issuer-uri 属性,还必须满足以下条件之一:[idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration)[idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer)[idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer) 是授权服务器支持的端点。此端点被称为 Provider Configuration 端点或 Authorization Server Metadata 端点。

启动预期

当使用此属性和这些依赖项时,资源服务器会自动配置自己以验证JWT编码的Bearer令牌。

它通过一个确定性的启动过程来实现这一点:

  1. 访问 Provider Configuration 或 Authorization Server Metadata 端点,处理响应以获取 jwks_url 属性。

  2. 配置验证策略以查询 jwks_url 获取有效的公钥。

  3. 配置验证策略以验证每个 JWT 的 iss 声明是否与 [idp.example.com](https://idp.example.com) 匹配。

这一过程的结果是,授权服务器必须能够接收请求,以便资源服务器能够成功启动。

备注

如果在资源服务器查询时(给定适当的超时时间),授权服务器宕机,则启动将失败。

运行时预期

一旦应用程序启动,资源服务器将尝试处理任何包含 Authorization: Bearer 头的请求:

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
html

只要指定了此方案,资源服务器就会尝试根据Bearer Token规范处理请求。

给定一个格式良好的 JWT,资源服务器:

  1. 在启动时从 jwks_url 端点获取公钥,并与 JWT 的头部进行匹配,以验证其签名。

  2. 验证 JWT 的 expnbf 时间戳以及 JWT 的 iss 声明。

  3. 将每个范围映射为带有前缀 SCOPE_ 的权限。

备注

当授权服务器提供新的密钥时,Spring Security 会自动轮换用于验证 JWT 令牌的密钥。

默认情况下,生成的 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
yaml
备注

JWK 集合 uri 没有标准化,但你通常可以在授权服务器的文档中找到它。

因此,资源服务器在启动时不会 ping 授权服务器。我们仍然指定 issuer-uri,以便资源服务器仍然验证传入 JWT 中的 iss 声明。

备注

你可以直接在DSL中提供此属性。

覆盖或替换启动自动配置

Spring Boot 为资源服务器生成两个 @Bean 对象。

第一个 bean 是一个 SecurityWebFilterChain,它将应用程序配置为资源服务器。当包含 spring-security-oauth2-jose 时,这个 SecurityWebFilterChain 看起来像:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
java

如果应用程序没有暴露 SecurityWebFilterChain bean,Spring Boot 会暴露默认的 SecurityWebFilterChain(如前面的代码清单所示)。

要替换它,在应用程序中暴露 @Bean

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();
}
java

上述配置要求任何以 /messages/ 开头的 URL 都需要 message:read 范围。

oauth2ResourceServer DSL 中的方法也会覆盖或替换自动配置。

例如,Spring Boot 创建的第二个 @BeanReactiveJwtDecoder,它将 String 类型的令牌解码为已验证的 Jwt 实例:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
java
备注

调用 ReactiveJwtDecoders#fromIssuerLocation 会调用 Provider Configuration 或 Authorization Server Metadata 端点来获取 JWK Set URI。如果应用程序没有暴露 ReactiveJwtDecoder bean,Spring Boot 会暴露上述默认的 bean。

其配置可以通过使用 jwkSetUri() 覆盖,或者通过使用 decoder() 替换。

使用 jwkSetUri()

您可以将授权服务器的 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();
}
java

使用 jwkSetUri() 优先于任何配置属性。

使用 decoder()

decoder()jwkSetUri() 更强大,因为它完全替换了 Spring Boot 对 JwtDecoder 的任何自动配置:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
return http.build();
}
java

这在你需要更深层次的配置时非常方便,例如验证

暴露一个 ReactiveJwtDecoder @Bean

或者,暴露一个 ReactiveJwtDecoder @Beandecoder() 具有相同的效果:你可以使用 jwkSetUri 构造一个,如下所示:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
java

或者你可以使用 issuer,让 NimbusReactiveJwtDecoder 在调用 build() 时查找 jwkSetUri,如下所示:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
}
java

或者,如果默认设置适合你,你也可以使用 JwtDecoders,它除了配置解码器的验证器之外,还执行上述操作:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuer);
}
java

配置可信算法

默认情况下,NimbusReactiveJwtDecoder(因此资源服务器)仅信任和验证使用 RS256 的令牌。

您可以使用 Spring Boot 或通过使用 the NimbusJwtDecoder 构建器 来自定义此行为。

使用 Spring Boot 自定义可信算法

设置算法的最简单方法是将其作为属性:

<SomeComponent algorithm="simpleAlgorithm" />
mdx
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
yaml

使用构建器自定义可信算法

不过,为了获得更大的功能,我们可以使用与 NimbusReactiveJwtDecoder 一起提供的构建器:

@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
java

多次调用 jwsAlgorithm 会配置 NimbusReactiveJwtDecoder 以信任多个算法:

@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
java

或者,你可以调用 jwsAlgorithms

@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
java

信任单个非对称密钥

使用硬编码的RSA公钥比通过JWK Set端点支持资源服务器更简单。可以通过Spring Boot使用构建器提供公钥。

通过 Spring Boot

你可以用 Spring Boot 指定一个密钥:

spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
yaml

或者,为了实现更复杂的查找,您可以对 RsaKeyConversionServicePostProcessor 进行后处理:

@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
java

指定密钥的位置:

key.location: hfds://my-key.pub
yaml

然后自动装配该值:

@Value("${key.location}")
RSAPublicKey key;
java

使用构建器

要直接连接一个 RSAPublicKey,请使用适当的 NimbusReactiveJwtDecoder 构建器:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
}
java

信任单个对称密钥

您也可以使用单个对称密钥。您可以加载您的 SecretKey,并使用适当的 NimbusReactiveJwtDecoder 构造器:

@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}
java

配置授权

由 OAuth 2.0 授权服务器颁发的 JWT 通常具有 scopescp 属性,表示已授予的作用域(或权限) — 例如:

{ ..., "scope" : "messages contacts"}
json

在这种情况下,资源服务器会尝试将这些范围强制转换为授权列表,在每个范围前加上字符串 SCOPE_

这意味着,要使用从 JWT 派生的范围来保护端点或方法,相应的表达式应包含此前缀:

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();
}
java

你可以对方法安全性做类似的事情:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
java

手动提取授权

然而,有多种情况下这种默认设置是不够的。例如,一些授权服务器不使用 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);
}
java

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());
}
}
java

为了获得更大的灵活性,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);
}
}
java

配置验证

使用最小的 Spring Boot 配置,指定授权服务器的 issuer URI,资源服务器默认会验证 iss 声明以及 expnbf 时间戳声明。

在需要自定义验证需求的情况下,Resource Server 提供了两个标准验证器,并且也接受自定义的 OAuth2TokenValidator 实例。

自定义时间戳验证

JWT 实例通常具有一个有效期窗口,窗口的开始时间由 nbf 声明指示,结束时间由 exp 声明指示。

然而,每个服务器都可能会出现时钟漂移,这会导致一个服务器认为令牌已过期,而另一个服务器则不这么认为。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现上的问题。

资源服务器使用 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;
}
java
备注

默认情况下,资源服务器配置了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);
}
}
}
java

然后,要添加到资源服务器中,您可以指定 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;
}
java