# OAuth2.0 资源服务器 JWT

# JWT 的最小依赖项

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

# JWTS 的最小配置

当使用Spring Boot (opens new window)时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。

# 指定授权服务器

在 Spring 引导应用程序中,要指定使用哪个授权服务器,只需执行以下操作:

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

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

要使用issuer-uri属性,还必须证明[https://idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration)[https://idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer)[https://idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer)中的一个是授权服务器所支持的端点。
此端点被称为提供者配置 (opens new window)端点或授权服务器元数据 (opens new window)端点。

就这样!

# 创业期望

当使用此属性和这些依赖项时,Resource Server 将自动配置自身以验证 JWT 编码的承载令牌。

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

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

  2. 将验证策略配置为查询jwks_url中的有效公钥

  3. 将验证策略配置为针对[https://idp.example.com](https://idp.example.com)的每个 JWTSiss索赔进行验证。

此过程的结果是,为了使资源服务器成功启动,授权服务器必须启动并接收请求。

如果在资源服务器查询时,授权服务器处于关闭状态(给定适当的超时),那么启动将失败。

# 运行时期望

一旦启动应用程序,Resource Server 将尝试处理任何包含Authorization: Bearer报头的请求:

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

只要表明了该方案,资源服务器就会尝试根据承载令牌规范来处理请求。

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

  1. 根据在启动过程中从jwks_url端点获得并与 JWTS 头匹配的公钥验证其签名

  2. 验证 JWTSexpnbf时间戳和 JWTSiss声明,并

  3. 将每个作用域映射到一个前缀SCOPE_的权限。

由于授权服务器提供了可用的新密钥, Spring 安全性将自动旋转用于验证 JWT 令牌的密钥。

默认情况下,生成的Authentication#getPrincipal是 Spring securityJwt对象,并且Authentication#getName映射到 JWT 的sub属性(如果存在)。

从这里开始,考虑跳到:

如何在不将资源服务器启动与授权服务器的可用性绑定的情况下进行配置

How to Configure without Spring Boot

# 直接指定授权服务器 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,以便 Resource Server 仍然在传入的 JWTS 上验证iss声明。

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

# 覆盖或替换 Boot Auto 配置

有两个@Beans, Spring boot 代表资源服务器生成。

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

例 1。资源服务器 SecurityWebFilterchain

爪哇

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

Kotlin

@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
}

如果应用程序不公开SecurityWebFilterChain Bean,那么 Spring 引导将公开上面的默认引导。

替换它就像在应用程序中公开 Bean 一样简单:

例 2。替换 SecurityWebFilterchain

爪哇

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

Kotlin

@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize("/message/**", hasAuthority("SCOPE_message:read"))
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
}

对于任何以/messages/开头的 URL,上述条件要求message:read的范围。

oauth2ResourceServerDSL 上的方法也将覆盖或替换自动配置。

例如,第二个@Bean Spring 引导创建了一个ReactiveJwtDecoder,它将String令牌解码为Jwt的验证实例:

例 3。ReactiveJWTdecoder

爪哇

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

Kotlin

@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
调用[ReactiveJwtDecoders#fromIssuerLocation](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-)是调用提供者配置或授权服务器元数据端点的目的,以便派生 JWK 集 URI。
如果应用程序不公开ReactiveJwtDecoder Bean,那么 Spring 引导将公开上述默认的。

并且其配置可以使用jwkSetUri()重写或使用decoder()替换。

# `

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

Kotlin

@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt {
                jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
            }
        }
    }
}

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

# `

jwkSetUri()更强大的是decoder(),它将完全取代JwtDecoder的任何引导自动配置:

爪哇

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

Kotlin

@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt {
                jwtDecoder = myCustomDecoder()
            }
        }
    }
}

当需要更深的配置时,比如validation,这是很方便的。

# 曝光ReactiveJwtDecoder``@Bean

或者,暴露ReactiveJwtDecoder``@Bean具有与decoder()相同的效果:

爪哇

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

Kotlin

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

# 配置可信算法

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

你可以通过Spring BootNimbusJWTDecoder Builder对此进行自定义。

# 通过 Spring 引导

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

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

# 使用构建器

不过,为了获得更大的功率,我们可以使用带有NimbusReactiveJwtDecoder的建造器:

爪哇

@Bean
ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).build();
}

Kotlin

@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).build()
}

多次调用jwsAlgorithm将配置NimbusReactiveJwtDecoder来信任多个算法,如下所示:

爪哇

@Bean
ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}

Kotlin

@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}

或者,你可以调用jwsAlgorithms:

爪哇

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

Kotlin

@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
            .jwsAlgorithms {
                it.add(RS512)
                it.add(ES512)
            }
            .build()
}

# 信任单一的非对称密钥

比支持具有 JWK 设置端点的资源服务器更简单的方法是对 RSA 公钥进行硬编码。公钥可以通过Spring Boot使用构建器提供。

# 通过 Spring 引导

通过 Spring 引导指定一个键非常简单。密钥的位置可以这样指定:

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

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

例 4。BeanFactoryPostprocessor

爪哇

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

Kotlin

@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
    return BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory ->
        beanFactory.getBean<RsaKeyConversionServicePostProcessor>()
                .setResourceLoader(CustomResourceLoader())
    }
}

指定你的密钥的位置:

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

然后自动连接该值:

爪哇

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

Kotlin

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

# 使用构建器

要直接连接RSAPublicKey,只需使用适当的NimbusReactiveJwtDecoder构建器,如下所示:

爪哇

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

Kotlin

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

# 信任单一的对称密钥

使用单一的对称密钥也很简单。你可以简单地在SecretKey中加载,并使用适当的NimbusReactiveJwtDecoder构建器,如下所示:

爪哇

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

Kotlin

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

# 配置授权

从 OAuth2.0 授权服务器发出的 JWT 通常具有scopescp属性,指示已授予的范围(或权限),例如:

{ …​, "scope" : "messages contacts"}

在这种情况下,Resource Server 将尝试强制将这些作用域放入一个已授予权限的列表中,并在每个作用域前加上字符串“scope_”。

这意味着,要使用从 JWT 派生的作用域来保护端点或方法,相应的表达式应该包括以下前缀:

爪哇

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

Kotlin

@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
            authorize("/messages/**", hasAuthority("SCOPE_messages"))
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
}

或类似于方法安全性:

爪哇

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

Kotlin

@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): Flux<Message> { }

# 手动提取权限

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

Kotlin

@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)
}

它负责将Jwt转换为Authentication。作为其配置的一部分,我们可以提供一个从JwtCollection的辅助转换器。

最后的转换器可能类似于下面的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());
    }
}

Kotlin

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>>的任何类完全替换转换器:

爪哇

static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return Mono.just(jwt).map(this::doConversion);
    }
}

Kotlin

internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
    override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
        return Mono.just(jwt).map(this::doConversion)
    }
}

# 配置验证

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

在需要定制验证的情况下,Resource Server 附带两个标准验证器,并且还接受定制的OAuth2TokenValidator实例。

# 自定义时间戳验证

JWT 通常有一个有效窗口,窗口的开始在nbf声明中指示,结束在exp声明中指示。

然而,每个服务器都可能经历时钟漂移,这可能导致令牌在一台服务器上出现过期,而不是在另一台服务器上出现。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现令人心烦。

Resource Server 使用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;
}

Kotlin

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

# 配置自定义验证器

使用OAuth2TokenValidatorAPI 添加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);
        }
    }
}

Kotlin

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实例:

爪哇

@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;
}

Kotlin

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

OAuth2 资源服务器不透明令牌