OAuth 2.0 Resource Server JWT

Minimal Dependencies for JWT

大部分资源服务器支持都收集在 spring-security-oauth2-resource-server 中。但是,解码和验证 JWT 的支持位于 spring-security-oauth2-jose 中,这意味着同时具有这两者才能拥有支持经过 JWT 编码的 Bearer 令牌的可运作的资源服务器。

Minimal Configuration for JWTs

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

Specifying the Authorization Server

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

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

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

Startup Expectations

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

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

  1. 点击提供商配置或授权服务器元数据端点,处理用于 `jwks_url`属性的响应。

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

  3. 将验证策略配置为根据 https://idp.example.com 验证每个 JWT 的 iss 声明。

此过程的后果是,授权服务器必须正接收请求,资源服务器才能成功启动。

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

Runtime Expectations

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

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

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

鉴于合法的 JWT,资源服务器:

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

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

  3. 使用前缀 SCOPE_ 将每个范围映射到一个权限。

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

默认情况下,结果 Authentication#getPrincipal 是 Spring Security Jwt 对象,如果存在 Authentication#getName,则映射到 JWT 的 sub 属性。

从此处,考虑跳转到:

Specifying the Authorization Server JWK Set Uri Directly

如果授权服务器不支持任何配置端点,或者如果资源服务器能够独立于授权服务器启动,您还可以提供 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 仍验证传入 JWT 上的 iss 声明。

您可以直接在 DSL 上提供此属性。

Overriding or Replacing Boot Auto Configuration

Spring Boot 会代表 Resource Server 生成两个 @Bean 对象。

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

Resource Server 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

Replacing SecurityWebFilterChain
  • 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 创建的第二个 @BeanReactiveJwtDecoder,它将 String 令牌解码为 Jwt 的验证实例:

ReactiveJwtDecoder
  • Java

  • Kotlin

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}

调用 {security-api-url}org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-[ReactiveJwtDecoders#fromIssuerLocation] 会调用提供者配置或授权服务器元数据端点以导出 JWK 集 URI。如果应用程序不公开 ReactiveJwtDecoder Bean,那么 Spring Boot 会公开上述默认的 bean。

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

Using jwkSetUri()

您可以配置授权服务器的 JWK 设置 URI as a configuration property 或在 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() 优先于任何配置属性。

Using decoder()

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

  • 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()
            }
        }
    }
}

这在需要更深入的配置时很方便,例如 validation

Exposing a ReactiveJwtDecoder @Bean

或者,公开一个 ReactiveJwtDecoder @Beandecoder() 具有相同的效果:您可以通过 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)
}

Configuring Trusted Algorithms

默认情况下,NimbusReactiveJwtDecoder 以及 Resource Server 仅信任并验证使用 RS256 的令牌。

您可以使用 Spring Bootthe NimbusJwtDecoder builder 自定义此行为。

Customizing Trusted Algorithms with Spring Boot

设置算法的最简单方法是用作属性:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithms: RS512
          jwk-set-uri: https://idp.example.org/.well-known/jwks.json

Customizing Trusted Algorithms by Using a Builder

不过,为了获得更大的功能,我们可以使用 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()
}

Trusting a Single Asymmetric Key

将 RSA 公钥硬编码比使用 JWK 集端点为资源服务器提供支持更容易。可以使用 Spring BootUsing a Builder 提供公钥。

Via Spring Boot

你可以使用 Spring Boot 指定密钥:

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

或者,为了允许执行更复杂的查找,可以对 RsaKeyConversionServicePostProcessor 执行后处理:

BeanFactoryPostProcessor
  • 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

Using a Builder

如要直接连接 RSAPublicKey,请使用相应的 NimbusReactiveJwtDecoder 生成器:

  • Java

  • Kotlin

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

Trusting a Single Symmetric Key

你还可以使用单个对称密钥。你可以加载所述 SecretKey 并使用相应的 NimbusReactiveJwtDecoder 生成器:

  • Java

  • Kotlin

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

Configuring Authorization

通常,OAuth 2.0 授权服务器授予的 JWT 具有 scopescp 属性,指示它已被授予作用域(或权限),例如:

{ ..., "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> { }

Extracting Authorities Manually

然而,在这种默认情况下有很多不足之处。例如,某些授权服务器不使用 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)
    }
}

Configuring Validation

使用 minimal Spring Boot configuration 指示授权服务器的发行方 URI,资源服务器默认验证 iss 声明以及 expnbf 时间戳声明。

在需要自定义验证需求的情况下,资源服务器将随附两个标准验证器,并且还接受自定义 OAuth2TokenValidator 实例。

Customizing Timestamp Validation

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 秒。

Configuring a Custom Validator

您可以使用 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
}