OAuth 2.0 资源服务器 JWT

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

JWT 的最小依赖

大多数资源服务器支持被收集到 spring-security-oauth2-resource-server 中。然而,对JWT的解码和验证的支持在 spring-security-oauth2-jose 中,这意味着要有一个支持JWT编码的 Bearer Token 的工作资源服务器,两者都是必要的。

JWT的最基本配置

使用 Spring Boot 时,将一个应用程序配置为资源服务器包括两个基本步骤。首先,添加所需的依赖。第二,指定授权服务器的位置。

指定授权服务器

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

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

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

最开始的期待(Startup Expectations)

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

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

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

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

  3. 配置验证策略,以验证每个 JWT 的 iss claim 与 idp.example.com

这个过程的后果是,授权服务器必须接收请求,以使资源服务器成功启动。

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

运行时异常 (Runtime Expectation)

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

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

只要这个 scheme 被指明,资源服务器就会尝试根据 Bearer Toke 规范来处理请求。

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

  1. 根据启动期间从 jwks_url 端点获得的公钥验证其签名,并与 JWT 头匹配。

  2. 验证JWT expnbf 时间戳以及 JWT iss claim。

  3. 将每个scope映射到一个前缀为 SCOPE_ 的授权。

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

默认情况下,产生的 Authentication#getPrincipal 是一个Spring Security Jwt 对象,Authentication#getName 映射到 JWTsub 属性,如果有的话。

从这里,考虑跳转到:

直接指定授权服务器 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,这样资源服务器仍然会验证传入的JWTs的 iss claim。

你可以直接在DSL上提供这个属性。

覆盖或取代启动自动配置

Spring Boot 代表资源服务器生成了两个 @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

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		.authorizeExchange(exchanges -> exchanges
			.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
			.anyExchange().authenticated()
		)
		.oauth2ResourceServer(oauth2 -> oauth2
			.jwt(withDefaults())
		);
	return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize("/message/**", hasAuthority("SCOPE_message:read"))
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
}

前面的配置要求任何以 /messages/ 开头的URL的 scope 为 message:read

oauth2ResourceServer DSL上的方法也覆盖或替代自动配置。

例如,Spring Boot 创建的第二个 @BeanReactiveJwtDecoder,它将 String token 解码为 Jwt 的验证实例。

ReactiveJwtDecoder
  • Java

  • Kotlin

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

调用 ReactiveJwtDecoders#fromIssuerLocation 调用提供者配置或授权服务器元数据端点来推导JWK Set URI。如果应用程序没有公开 ReactiveJwtDecoder Bean,Spring Boot 就会公开上述默认的。

它的配置可以通过使用 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() 更强大,因为它完全取代了 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()
            }
        }
    }
}

当你需要更深入的配置时,例如验证,这很方便。

暴露 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,让 NimbusReactiveJwtDecoderbuild() 被调用时查找 jwkSetUri,如下所示:

  • Java

  • Kotlin

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

或者,如果默认值对你有效,你也可以使用 JwtDecoders,它除了配置 decoder 的 validator 外,还可以做上述工作:

  • Java

  • Kotlin

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

配置受信任的算法(Trusted Algorithms)

默认情况下,NimbusReactiveJwtDecoder,以及资源服务器,只信任和验证使用 RS256 的令牌。

你可以用Spring Boot或通过使用 NimbusJwtDecoder builder 来定制这种行为。

用 Spring Boot 定制受信任的算法

设置算法的最简单方法是作为一个属性。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithm: 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 提供,也可以通过使用 Builder提供。

通过 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 的位置。

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

自动注入值。

  • Java

  • Kotlin

@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null

使用 Builder

要直接注入一个 RSAPublicKey,请使用适当的 NimbusReactiveJwtDecoder builder。

  • Java

  • Kotlin

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

信任单一对称密钥

你也可以使用单一的对称密钥。你可以加载你的 SecretKey 并使用适当的 NimbusReactiveJwtDecoder builder。

  • 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“胁迫”成一个授予权限的列表,在每个scope前加上字符串 SCOPE_

这意味着,为了保护具有源自JWT的范围的端点或方法,相应的表达式应该包括这个前缀。

  • Java

  • Kotlin

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		.authorizeExchange(exchanges -> exchanges
			.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
			.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
			.anyExchange().authenticated()
		)
		.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
    return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
            authorize("/messages/**", hasAuthority("SCOPE_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 属性。相反,他们有自己的自定义属性。在其他时候,资源服务器可能需要将属性或属性的构成调整为内部化的授权。

为此,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> 的类来完全替换 converter。

  • 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配置,表明授权服务器的 issuer URI,资源服务器默认验证 iss claim 以及 expnbf 时间戳索赔。

在你需要定制验证需求的情况下,资源服务器提供了两个标准的验证器,同时也接受自定义的 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 claim 的检查。

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