此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.1spring-doc.cadn.net.cn

OAuth 2.0 资源服务器 JWT

JWT 的最小依赖项

大多数 Resource Server 支持都收集到spring-security-oauth2-resource-server. 但是,对解码和验证 JWT 的支持在spring-security-oauth2-jose,这意味着两者都是必要的,以便拥有支持 JWT 编码的 Bearer Token 的工作资源服务器。spring-doc.cadn.net.cn

JWT 的最低配置

使用 Spring Boot 时,将应用程序配置为资源服务器包括两个基本步骤。 首先,包括所需的依赖项,其次,指示授权服务器的位置。spring-doc.cadn.net.cn

指定 Authorization Server

在 Spring Boot 应用程序中,要指定要使用的授权服务器,只需执行以下作:spring-doc.cadn.net.cn

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

哪里idp.example.com/issuerissAuthorization Server 将颁发的 JWT 令牌的声明。 Resource Server 将使用此属性进一步进行自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。spring-doc.cadn.net.cn

就是这样!spring-doc.cadn.net.cn

创业期望

使用此属性和这些依赖项时,Resource Server 将自动配置自身以验证 JWT 编码的不记名令牌。spring-doc.cadn.net.cn

它通过确定性启动过程来实现这一点:spring-doc.cadn.net.cn

  1. 点击 Provider Configuration 或 Authorization Server Metadata 端点,处理jwks_url财产spring-doc.cadn.net.cn

  2. 配置验证策略以查询jwks_url对于有效的公钥spring-doc.cadn.net.cn

  3. 配置验证策略以验证每个 JWTiss索赔idp.example.com.spring-doc.cadn.net.cn

此过程的结果是,授权服务器必须启动并接收请求,Resource Server 才能成功启动。spring-doc.cadn.net.cn

如果 Resource Server 查询授权服务器时授权服务器已关闭(给定适当的超时),则启动将失败。

运行时预期

应用程序启动后,Resource Server 将尝试处理任何包含Authorization: Bearer页眉:spring-doc.cadn.net.cn

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

只要指定了此方案,Resource Server 就会尝试根据 Bearer Token 规范处理请求。spring-doc.cadn.net.cn

给定格式正确的 JWT,Resource Server 将:spring-doc.cadn.net.cn

  1. 根据从jwks_urlendpoint,并与 JWTs 标头匹配spring-doc.cadn.net.cn

  2. 验证 JWTexpnbftimestamp 和 JWTissclaim 和spring-doc.cadn.net.cn

  3. 将每个范围映射到带有前缀的颁发机构SCOPE_.spring-doc.cadn.net.cn

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

结果Authentication#getPrincipal默认情况下,是 Spring SecurityJwtobject 和Authentication#getName映射到 JWT 的sub属性(如果存在)。spring-doc.cadn.net.cn

从这里,考虑跳到:spring-doc.cadn.net.cn

直接指定授权服务器 JWK 集 URI

如果授权服务器不支持任何配置端点,或者如果 Resource Server 必须能够独立于授权服务器启动,则jwk-set-uri也可以提供:spring-doc.cadn.net.cn

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 的声明。spring-doc.cadn.net.cn

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

覆盖或替换引导自动配置

有两个@Bean的 Spring Boot 代表 Resource Server 生成的。spring-doc.cadn.net.cn

第一个是SecurityWebFilterChain,将应用程序配置为资源服务器。当包含spring-security-oauth2-joseSecurityWebFilterChain看来:spring-doc.cadn.net.cn

资源服务器安全性WebFilterChain
@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 { }
        }
    }
}

如果应用程序没有公开SecurityWebFilterChainbean,则 Spring Boot 将公开上述默认的 bean。spring-doc.cadn.net.cn

替换它就像在应用程序中公开 bean 一样简单:spring-doc.cadn.net.cn

替换 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();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        authorizeExchange {
            authorize("/message/**", hasAuthority("SCOPE_message:read"))
            authorize(anyExchange, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
}

以上要求message:read对于任何以/messages/.spring-doc.cadn.net.cn

方法oauth2ResourceServerDSL 还将覆盖或替换 auto 配置。spring-doc.cadn.net.cn

例如,第二个@BeanSpring Boot 创建的是一个ReactiveJwtDecoder,它解码String令牌转换为已验证的Jwt:spring-doc.cadn.net.cn

ReactiveJwtDecoder
@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
ReactiveJwtDecoders#fromIssuerLocation是调用 Provider Configuration 或 Authorization Server Metadata 端点以派生 JWK Set Uri 的内容。 如果应用程序没有公开ReactiveJwtDecoderbean,则 Spring Boot 将公开上述默认的 bean。

并且它的配置可以使用jwkSetUri()或使用decoder().spring-doc.cadn.net.cn

jwkSetUri()

授权服务器的 JWK 集 URI 可以配置为配置属性,也可以在 DSL 中提供:spring-doc.cadn.net.cn

@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()优先于任何配置属性。spring-doc.cadn.net.cn

decoder()

jwkSetUri()decoder(),它将完全替换JwtDecoder:spring-doc.cadn.net.cn

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

当需要更深入的配置(如验证)时,这很方便。spring-doc.cadn.net.cn

公开ReactiveJwtDecoder @Bean

或者,公开ReactiveJwtDecoder @Bean具有相同的效果decoder():spring-doc.cadn.net.cn

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

配置可信算法

默认情况下,NimbusReactiveJwtDecoder,因此 Resource Server 将仅使用RS256.spring-doc.cadn.net.cn

您可以通过 Spring BootNimbusJwtDecoder 构建器对其进行自定义。spring-doc.cadn.net.cn

通过 Spring Boot

设置算法的最简单方法是 as a property:spring-doc.cadn.net.cn

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

使用 Builder

不过,为了获得更大的功能,我们可以使用附带NimbusReactiveJwtDecoder:spring-doc.cadn.net.cn

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

jwsAlgorithm将配置NimbusReactiveJwtDecoder信任多个算法,如下所示:spring-doc.cadn.net.cn

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

或者,您可以调用jwsAlgorithms:spring-doc.cadn.net.cn

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

信任单个非对称密钥

比使用 JWK Set 端点支持 Resource Server 更简单的方法是对 RSA 公钥进行硬编码。 公钥可以通过 Spring Boot 或使用 Builder 提供。spring-doc.cadn.net.cn

通过 Spring Boot

通过 Spring Boot 指定密钥非常简单。 键的位置可以像这样指定:spring-doc.cadn.net.cn

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

或者,为了实现更复杂的查找,您可以对RsaKeyConversionServicePostProcessor:spring-doc.cadn.net.cn

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

指定密钥的位置:spring-doc.cadn.net.cn

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

然后自动装配该值:spring-doc.cadn.net.cn

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

使用 Builder

要连接RSAPublicKey直接,您可以简单地使用适当的NimbusReactiveJwtDecoderbuilder 中,如下所示:spring-doc.cadn.net.cn

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

信任单个对称密钥

使用单个对称密钥也很简单。 您只需加载SecretKey并使用适当的NimbusReactiveJwtDecoderbuilder 中,如下所示:spring-doc.cadn.net.cn

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

配置授权

从 OAuth 2.0 授权服务器颁发的 JWT 通常具有scopescp属性,指示已授予的范围(或权限),例如:spring-doc.cadn.net.cn

{ …​, "scope" : "messages contacts"}spring-doc.cadn.net.cn

在这种情况下,Resource Server 将尝试将这些范围强制转换为已授予的权限列表,并在每个范围前加上字符串“SCOPE_”。spring-doc.cadn.net.cn

这意味着,要保护具有从 JWT 派生的作用域的端点或方法,相应的表达式应包含以下前缀:spring-doc.cadn.net.cn

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

或者与方法安全性类似:spring-doc.cadn.net.cn

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

手动提取权限

但是,在许多情况下,这种违约是不够的。 例如,某些授权服务器不使用scope属性,而是拥有自己的自定义属性。 或者,在其他时候,资源服务器可能需要将属性或属性组合调整为内部化权限。spring-doc.cadn.net.cn

为此,DSL 公开了jwtAuthenticationConverter():spring-doc.cadn.net.cn

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

它负责将Jwt转换为Authentication. 作为其配置的一部分,我们可以提供一个辅助转换器,从Jwt更改为Collection的授予权限。spring-doc.cadn.net.cn

最终的转换器可能类似于GrantedAuthoritiesExtractor下面:spring-doc.cadn.net.cn

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>>:spring-doc.cadn.net.cn

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 将默认验证issclaim 以及expnbftimestamp 声明。spring-doc.cadn.net.cn

在需要自定义验证的情况下,Resource Server 附带了两个标准验证程序,并且还接受自定义OAuth2TokenValidator实例。spring-doc.cadn.net.cn

自定义时间戳验证

JWT 通常有一个有效期窗口,窗口的开始在nbfclaim 和exp索赔。spring-doc.cadn.net.cn

但是,每个服务器都可能遇到 clock drift,这可能导致令牌对一个服务器显示已过期,但对另一个服务器则不然。 随着分布式系统中协作服务器数量的增加,这可能会导致一些实施 Burn。spring-doc.cadn.net.cn

Resource Server 使用JwtTimestampValidator验证令牌的有效性窗口,并且可以使用clockSkew为了缓解上述问题:spring-doc.cadn.net.cn

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

配置自定义验证器

audclaim 很简单,使用OAuth2TokenValidator应用程序接口:spring-doc.cadn.net.cn

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实例:spring-doc.cadn.net.cn

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