# OAuth2.0资源服务器JWT ## JWT的最小依赖项 大多数资源服务器支持被收集到`spring-security-oauth2-resource-server`中。然而,对JWTS的解码和验证的支持是`spring-security-oauth2-jose`,这意味着这两个都是必要的,以便拥有一个支持JWT编码的承载令牌的工作资源服务器。 ## JWTS的最小配置 当使用[Spring Boot](https://spring.io/projects/spring-boot)时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。 ### 指定授权服务器 在 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)`中的一个是授权服务器所支持的端点。
此端点被称为[提供者配置](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)端点或[授权服务器元数据](https://tools.ietf.org/html/rfc8414#section-3)端点。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 就这样! ### 创业期望 当使用此属性和这些依赖项时,Resource Server将自动配置自身以验证JWT编码的承载令牌。 它通过一个确定性的启动过程来实现这一点: 1. 查询`jwks_url`属性的提供者配置或授权服务器元数据端点 2. 查询`jwks_url`端点以获得支持的算法 3. 将验证策略配置为查询`jwks_url`中找到的算法的有效公钥 4. 将验证策略配置为针对`[https://idp.example.com](https://idp.example.com)`的每个JWTS`iss`索赔进行验证。 此过程的结果是,为了使资源服务器成功启动,授权服务器必须启动并接收请求。 | |如果在资源服务器查询时,授权服务器处于关闭状态(给定适当的超时),那么启动将失败。| |---|-------------------------------------------------------------------------------------------------------------------------| ### 运行时期望 一旦启动应用程序,Resource Server将尝试处理任何包含`Authorization: Bearer`报头的请求: ``` GET / HTTP/1.1 Authorization: Bearer some-token-value # Resource Server will process this ``` 只要表明了该方案,资源服务器就会尝试根据承载令牌规范来处理请求。 给定一个格式良好的JWT,资源服务器将: 1. 根据在启动过程中从`jwks_url`端点获得并与JWT匹配的公钥验证其签名 2. 验证JWT的`exp`和`nbf`时间戳以及JWT的`iss`声明,并 3. 将每个作用域映射到一个前缀`SCOPE_`的权限。 | |由于授权服务器提供了可用的新密钥, Spring 安全性将自动旋转用于验证JWTS的密钥。| |---|-------------------------------------------------------------------------------------------------------------------------------| 默认情况下,生成的`Authentication#getPrincipal`是 Spring security`Jwt`对象,并且`Authentication#getName`映射到JWT的`sub`属性(如果存在)。 从这里开始,考虑跳到: * [JWT身份验证的工作方式](#oauth2resourceserver-jwt-architecture) * [如何在不将资源服务器启动与授权服务器的可用性绑定的情况下进行配置](#oauth2resourceserver-jwt-jwkseturi) * [How to Configure without Spring Boot](#oauth2resourceserver-jwt-sansboot) ## JWT身份验证的工作方式 接下来,让我们看看 Spring Security在基于 Servlet 的应用程序中支持[JWT](https://tools.ietf.org/html/rfc7519)身份验证所使用的体系结构组件,就像我们刚才看到的那样。 [`JwtAuthenticationProvider`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/oauth2/server/resource/authentication/jwtauthenticationprovider.html)是一个[`AuthenticationProvider`](../.../././////认证/architectification/architecture.html# Servlet-authentification-authenticationprovider)实现,它利用了一个[<`JwtDecoder`](#oaut 让我们来看看`JwtAuthenticationProvider`在 Spring 安全性中是如何工作的。该图详细说明了`AuthenticationManager`](.../../authentication/architecture.html# Servlet-authentication-authenticationmanager)在[读取不记名令牌](#oauth2resourceserver-authentication-bearertokenauthenticationfilter)中的工作原理。 ![JWTAutHenticationProvider](https://docs.spring.io/spring-security/reference/_images/servlet/oauth2/jwtauthenticationprovider.png) 图1. `JwtAuthenticationProvider`用法 ![number 1](https://docs.spring.io/spring-security/reference/_images/icons/number_1.png)来自[读取不记名令牌](#oauth2resourceserver-authentication-bearertokenauthenticationfilter)的身份验证`Filter`将一个`BearerTokenAuthenticationToken`传递到`AuthenticationManager`,这是由[`ProviderManager`](.../../authentication/architecture.html# Servlet-authentication-providermanager)实现的。 ![number 2](https://docs.spring.io/spring-security/reference/_images/icons/number_2.png)`ProviderManager`被配置为使用[身份验证提供者](../../authentication/architecture.html#servlet-authentication-authenticationprovider)类型的`JwtAuthenticationProvider`。 ![number 3](https://docs.spring.io/spring-security/reference/_images/icons/number_3.png)`JwtAuthenticationProvider`使用[`JwtDecoder`](#OAuth2Resourceserver-JWT-decoder)对`Jwt`进行解码、验证和验证。 ![number 4](https://docs.spring.io/spring-security/reference/_images/icons/number_4.png)`JwtAuthenticationProvider`然后使用[`JwtAuthenticationConverter`](#OAuth2Resourceserver-JWT-Authorization-Extraction)将`Jwt`转换为授予权限的`Collection`。 ![number 5](https://docs.spring.io/spring-security/reference/_images/icons/number_5.png)当身份验证成功时,返回的[`Authentication`](.../../authentication/architecture.html# Servlet-authentication-authentication)类型为`JwtAuthenticationToken`,并且具有一个主体,即由配置的`JwtDecoder`返回的`Jwt`。最终,返回的`JwtAuthenticationToken`将由身份验证`Filter`设置在[`SecurityContextHolder`](.../authentication/architecture.html# Servlet-authentication-securitycontextholder)上。 ## 直接指定授权服务器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](#oauth2resourceserver-jwt-jwkseturi-dsl)上直接提供此属性。| |---|--------------------------------------------------------------------------------------------------| ## 覆盖或替换Boot Auto配置 有两个`@Bean`s, Spring boot代表资源服务器生成。 第一个是将应用程序配置为资源服务器的`WebSecurityConfigurerAdapter`。当包含`spring-security-oauth2-jose`时,这个`WebSecurityConfigurerAdapter`看起来是这样的: 例1.默认的JWT配置 Java ``` protected void configure(HttpSecurity http) { http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); } ``` Kotlin ``` fun configure(http: HttpSecurity) { http { authorizeRequests { authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { } } } } ``` 如果应用程序不公开`WebSecurityConfigurerAdapter` Bean,那么 Spring 引导将公开上面的默认引导。 替换它就像在应用程序中公开 Bean 一样简单: 例2.自定义JWT配置 Java ``` @EnableWebSecurity public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(myConverter()) ) ); } } ``` Kotlin ``` @EnableWebSecurity class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize("/messages/**", hasAuthority("SCOPE_message:read")) authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { jwtAuthenticationConverter = myConverter() } } } } } ``` 对于任何以`/messages/`开头的URL,上面要求`message:read`的范围。 `oauth2ResourceServer`DSL上的方法也将覆盖或替换自动配置。 例如,第二个`@Bean` Spring 引导创建了一个`JwtDecoder`,它[将`String`令牌解码为`Jwt`的验证实例](#OAuth2Resourceserver-jwt-architecture-jwtdecoder): 例3. JWT解码器 Java ``` @Bean public JwtDecoder jwtDecoder() { return JwtDecoders.fromIssuerLocation(issuerUri); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return JwtDecoders.fromIssuerLocation(issuerUri) } ``` | |调用`[JwtDecoders#fromIssuerLocation](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/jwt/JwtDecoders.html#fromIssuerLocation-java.lang.String-)`是调用提供程序配置或授权服务器元数据端点以派生JWK集URI的目的。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果应用程序不公开`JwtDecoder` Bean,那么 Spring 引导将公开上面的默认引导。 并且其配置可以使用`jwkSetUri()`重写或使用`decoder()`替换。 或者,如果根本不使用 Spring boot,那么这两个组件--过滤器链和`JwtDecoder`都可以用XML指定。 过滤链是这样指定的: 例4.默认的JWT配置 XML ``` ``` 而`JwtDecoder`就像这样: 例5. JWT解码器 XML ``` ``` ### ` 可以配置授权服务器的JWK集URI[作为配置属性](#oauth2resourceserver-jwt-jwkseturi),也可以在DSL中提供它: 例6. JWK设置URI配置 Java ``` @EnableWebSecurity public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwkSetUri("https://idp.example.com/.well-known/jwks.json") ) ); } } ``` Kotlin ``` @EnableWebSecurity class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { jwkSetUri = "https://idp.example.com/.well-known/jwks.json" } } } } } ``` XML ``` ``` 使用`jwkSetUri()`优先于任何配置属性。 ### ` 比`jwkSetUri()`更强大的是`decoder()`,它将完全取代[`JwtDecoder`](#OAuth2Resourceserver-jwt-architecture-jwtdecoder)的任何引导自动配置: 例7. JWT解码器配置 Java ``` @EnableWebSecurity public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .decoder(myCustomDecoder()) ) ); } } ``` Kotlin ``` @EnableWebSecurity class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { jwtDecoder = myCustomDecoder() } } } } } ``` XML ``` ``` 当需要更深的配置时,比如[validation](#oauth2resourceserver-jwt-validation)、[mapping](#oauth2resourceserver-jwt-claimsetmapping)或[请求超时](#oauth2resourceserver-jwt-timeouts),这是很方便的。 ### 曝光`JwtDecoder``@Bean` 或者,暴露[`JwtDecoder`](#OAuth2Resourceserver-JWT-Architecture-JWTDecoder)`@Bean`具有与`decoder()`相同的效果: Java ``` @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() } ``` ## 配置可信算法 默认情况下,`NimbusJwtDecoder`以及资源服务器将仅使用`RS256`信任和验证令牌。 你可以通过[Spring Boot](#oauth2resourceserver-jwt-boot-algorithm)、[NimbusJWTDecoder Builder](#oauth2resourceserver-jwt-decoder-builder)或从[JWK SET响应](#oauth2resourceserver-jwt-decoder-jwk-response)进行自定义。 ### 通过 Spring 引导 设置算法的最简单方法是作为一个属性: ``` spring: security: oauth2: resourceserver: jwt: jws-algorithm: RS512 jwk-set-uri: https://idp.example.org/.well-known/jwks.json ``` ### 使用构建器 不过,要获得更大的功率,我们可以使用带有`NimbusJwtDecoder`的建造器: Java ``` @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).build(); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).build() } ``` 多次调用`jwsAlgorithm`将配置`NimbusJwtDecoder`来信任多个算法,如下所示: Java ``` @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build(); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build() } ``` 或者,你可以调用`jwsAlgorithms`: Java ``` @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithms(algorithms -> { algorithms.add(RS512); algorithms.add(ES512); }).build(); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithms { it.add(RS512) it.add(ES512) }.build() } ``` ### 来自JWK SET响应 Spring 由于Security的JWT支持是基于Nimbus的,所以你也可以使用它的所有优秀功能。 例如,Nimbus有一个`JWSKeySelector`实现,该实现将基于JWK设置的URI响应来选择一组算法。你可以使用它生成`NimbusJwtDecoder`,如下所示: Java ``` @Bean public JwtDecoder jwtDecoder() { // makes a request to the JWK Set endpoint JWSKeySelector jwsKeySelector = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl); DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWSKeySelector(jwsKeySelector); return new NimbusJwtDecoder(jwtProcessor); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { // makes a request to the JWK Set endpoint val jwsKeySelector: JWSKeySelector = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl) val jwtProcessor: DefaultJWTProcessor = DefaultJWTProcessor() jwtProcessor.jwsKeySelector = jwsKeySelector return NimbusJwtDecoder(jwtProcessor) } ``` ## 信任单一的非对称密钥 比支持具有JWK设置端点的资源服务器更简单的方法是对RSA公钥进行硬编码。公钥可以通过[Spring Boot](#oauth2resourceserver-jwt-decoder-public-key-boot)或[使用构建器](#oauth2resourceserver-jwt-decoder-public-key-builder)提供。 ### 通过 Spring 引导 通过 Spring 引导指定一个键非常简单。密钥的位置可以这样指定: ``` spring: security: oauth2: resourceserver: jwt: public-key-location: classpath:my-key.pub ``` 或者,为了允许更复杂的查找,你可以对`RsaKeyConversionServicePostProcessor`进行后处理: Java ``` @Bean BeanFactoryPostProcessor conversionServiceCustomizer() { return beanFactory -> beanFactory.getBean(RsaKeyConversionServicePostProcessor.class) .setResourceLoader(new CustomResourceLoader()); } ``` Kotlin ``` @Bean fun conversionServiceCustomizer(): BeanFactoryPostProcessor { return BeanFactoryPostProcessor { beanFactory -> beanFactory.getBean() .setResourceLoader(CustomResourceLoader()) } } ``` 指定你的密钥的位置: ``` key.location: hfds://my-key.pub ``` 然后自动连接该值: Java ``` @Value("${key.location}") RSAPublicKey key; ``` Kotlin ``` @Value("\${key.location}") val key: RSAPublicKey? = null ``` ### 使用构建器 要直接连接`RSAPublicKey`,只需使用适当的`NimbusJwtDecoder`构建器,如下所示: Java ``` @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(this.key).build(); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return NimbusJwtDecoder.withPublicKey(this.key).build() } ``` ## 信任单一的对称密钥 使用单一的对称密钥也很简单。你可以简单地在`SecretKey`中加载,并使用适当的`NimbusJwtDecoder`构建器,如下所示: Java ``` @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withSecretKey(this.key).build(); } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { return NimbusJwtDecoder.withSecretKey(key).build() } ``` ## 配置授权 从OAuth2.0授权服务器发出的JWT通常具有`scope`或`scp`属性,指示已授予的范围(或权限),例如: `{ …​, "scope" : "messages contacts"}` 在这种情况下,Resource Server将尝试强制将这些作用域放入一个已授予权限的列表中,并在每个作用域前加上字符串“scope\_”。 这意味着,要使用从JWT派生的作用域来保护端点或方法,相应的表达式应该包括以下前缀: 例8.授权配置 Java ``` @EnableWebSecurity public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); } } ``` Kotlin ``` @EnableWebSecurity class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize("/contacts/**", hasAuthority("SCOPE_contacts")) authorize("/messages/**", hasAuthority("SCOPE_messages")) authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { } } } } } ``` XML ``` ``` 或类似于方法安全性: Java ``` @PreAuthorize("hasAuthority('SCOPE_messages')") public List getMessages(...) {} ``` Kotlin ``` @PreAuthorize("hasAuthority('SCOPE_messages')") fun getMessages(): List { } ``` ### 手动提取权限 然而,在许多情况下,这种默认设置是不够的。例如,一些授权服务器不使用`scope`属性,而是具有自己的自定义属性。或者,在其他时候,资源服务器可能需要将属性或属性的组合调整为内在化的权限。 为此, Spring Security附带`JwtAuthenticationConverter`,它负责[将`Jwt`转换为`Authentication`](#OAuth2Resourceserver-JWT-Architecture-JWTAutoThenticationConverter)。默认情况下, Spring 安全性将用`JwtAuthenticationProvider`的默认实例连接`JwtAuthenticationConverter`。 作为配置`JwtAuthenticationConverter`的一部分,你可以提供一个附属转换器,将其从`Jwt`转换为授予权限的`Collection`。 假设你的授权服务器在一个名为`authorities`的自定义声明中与权威机构通信。在这种情况下,你可以配置[`JwtAuthenticationConverter`](#OAuth2ResourceServer-JWT-Architecture-JWTAutoThenticationConverter)应该检查的声明,如下所示: 例9.当局声称配置 Java ``` @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } ``` Kotlin ``` @Bean fun jwtAuthenticationConverter(): JwtAuthenticationConverter { val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities") val jwtAuthenticationConverter = JwtAuthenticationConverter() jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) return jwtAuthenticationConverter } ``` XML ``` ``` 你也可以将权限前缀配置为不同的。你可以将它更改为`ROLE_`,而不是在每个权限前加上`SCOPE_`,就像这样: 例10.权限前缀配置 Java ``` @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } ``` Kotlin ``` @Bean fun jwtAuthenticationConverter(): JwtAuthenticationConverter { val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter() grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_") val jwtAuthenticationConverter = JwtAuthenticationConverter() jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) return jwtAuthenticationConverter } ``` XML ``` ``` 或者,你可以通过调用`JwtGrantedAuthoritiesConverter#setAuthorityPrefix("")`来完全删除前缀。 为了具有更大的灵活性,DSL支持用实现`Converter`的任何类完全替换转换器: Java ``` static class CustomAuthenticationConverter implements Converter { public AbstractAuthenticationToken convert(Jwt jwt) { return new CustomAuthenticationToken(jwt); } } // ... @EnableWebSecurity public class CustomAuthenticationConverterConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(new CustomAuthenticationConverter()) ) ); } } ``` Kotlin ``` internal class CustomAuthenticationConverter : Converter { override fun convert(jwt: Jwt): AbstractAuthenticationToken { return CustomAuthenticationToken(jwt) } } // ... @EnableWebSecurity class CustomAuthenticationConverterConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { jwtAuthenticationConverter = CustomAuthenticationConverter() } } } } } ``` ## 配置验证 使用[minimal Spring Boot configuration](#oauth2resourceserver-jwt-minimalconfiguration)(表示授权服务器的发行者URI),Resource Server将默认验证`iss`声明以及`exp`和`nbf`时间戳声明。 在需要定制验证的情况下,Resource Server附带两个标准验证器,并且还接受定制的`OAuth2TokenValidator`实例。 ### 自定义时间戳验证 JWT通常有一个有效窗口,窗口的开始在`nbf`索赔中指示,结束在`exp`索赔中指示。 然而,每个服务器都可能经历时钟漂移,这可能导致令牌在一台服务器上出现过期,而不是在另一台服务器上出现。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现令人心烦。 Resource Server使用`JwtTimestampValidator`来验证令牌的有效性窗口,并且可以将其配置为`clockSkew`以缓解上述问题: Java ``` @Bean JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri); OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( new JwtTimestampValidator(Duration.ofSeconds(60)), new JwtIssuerValidator(issuerUri)); jwtDecoder.setJwtValidator(withClockSkew); return jwtDecoder; } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder val withClockSkew: OAuth2TokenValidator = DelegatingOAuth2TokenValidator( JwtTimestampValidator(Duration.ofSeconds(60)), JwtIssuerValidator(issuerUri)) jwtDecoder.setJwtValidator(withClockSkew) return jwtDecoder } ``` | |默认情况下,Resource Server配置的时钟偏差为60秒。| |---|------------------------------------------------------------------| ### 配置自定义验证器 使用`OAuth2TokenValidator`API添加`aud`声明的检查很简单: Java ``` OAuth2TokenValidator audienceValidator() { return new JwtClaimValidator>(AUD, aud -> aud.contains("messaging")); } ``` Kotlin ``` fun audienceValidator(): OAuth2TokenValidator { return JwtClaimValidator>(AUD) { aud -> aud.contains("messaging") } } ``` 或者,为了获得更多的控制,你可以实现自己的`OAuth2TokenValidator`: Java ``` static class AudienceValidator implements OAuth2TokenValidator { OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null); @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { if (jwt.getAudience().contains("messaging")) { return OAuth2TokenValidatorResult.success(); } else { return OAuth2TokenValidatorResult.failure(error); } } } // ... OAuth2TokenValidator audienceValidator() { return new AudienceValidator(); } ``` Kotlin ``` internal class AudienceValidator : OAuth2TokenValidator { var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null) override fun validate(jwt: Jwt): OAuth2TokenValidatorResult { return if (jwt.audience.contains("messaging")) { OAuth2TokenValidatorResult.success() } else { OAuth2TokenValidatorResult.failure(error) } } } // ... fun audienceValidator(): OAuth2TokenValidator { return AudienceValidator() } ``` 然后,要添加到资源服务器中,需要指定[`JwtDecoder`](#OAuth2Resourceserver-jwt-architecture-jwtdecoder)实例: Java ``` @Bean JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri); OAuth2TokenValidator audienceValidator = audienceValidator(); OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); jwtDecoder.setJwtValidator(withAudience); return jwtDecoder; } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder val audienceValidator = audienceValidator() val withIssuer: OAuth2TokenValidator = JwtValidators.createDefaultWithIssuer(issuerUri) val withAudience: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) jwtDecoder.setJwtValidator(withAudience) return jwtDecoder } ``` ## 配置索赔集映射 Spring 安全性使用[Nimbus](https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home)库来解析JWTS并验证其签名。因此, Spring 安全性取决于Nimbus对每个字段值的解释以及如何将每个字段值强制为 Java 类型。 例如,因为Nimbus保持 Java 7兼容,所以它不使用`Instant`来表示时间戳字段。 而且完全有可能使用不同的库或用于JWT处理,这可能会做出自己的强制决策,需要进行调整。 或者,非常简单地说,出于特定于域的原因,资源服务器可能希望从JWT中添加或删除声明。 出于这些目的,Resource Server支持用`MappedJwtClaimSetConverter`映射JWT索赔集。 ### 定制单个索赔的转换 默认情况下,`MappedJwtClaimSetConverter`将尝试强制将声明转换为以下类型: |Claim|Java 类型| |-----|--------------------| |`aud`|`Collection`| |`exp`|`Instant`| |`iat`|`Instant`| |`iss`|`String`| |`jti`|`String`| |`nbf`|`Instant`| |`sub`|`String`| 可以使用`MappedJwtClaimSetConverter.withDefaults`配置单个索赔的转换策略: Java ``` @Bean JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub)); jwtDecoder.setClaimSetConverter(converter); return jwtDecoder; } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() val converter = MappedJwtClaimSetConverter .withDefaults(mapOf("sub" to this::lookupUserIdBySub)) jwtDecoder.setClaimSetConverter(converter) return jwtDecoder } ``` 这将保留所有的默认值,除了它将覆盖`sub`的默认索赔转换器。 ### 增加索赔 `MappedJwtClaimSetConverter`还可以用于添加自定义声明,例如,用于适应现有系统: Java ``` MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value")); ``` Kotlin ``` MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter { "value" })) ``` ### 删除索赔 删除声明也很简单,使用相同的API: Java ``` MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null)); ``` Kotlin ``` MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter { null })) ``` ### 重新命名索赔 在更复杂的场景中,例如一次查询多个声明或重命名一个声明,Resource Server接受实现`Converter, Map>`的任何类: Java ``` public class UsernameSubClaimAdapter implements Converter, Map> { private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); public Map convert(Map claims) { Map convertedClaims = this.delegate.convert(claims); String username = (String) convertedClaims.get("user_name"); convertedClaims.put("sub", username); return convertedClaims; } } ``` Kotlin ``` class UsernameSubClaimAdapter : Converter, Map> { private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()) override fun convert(claims: Map): Map { val convertedClaims = delegate.convert(claims) val username = convertedClaims["user_name"] as String convertedClaims["sub"] = username return convertedClaims } } ``` 然后,实例可以像正常情况一样提供: Java ``` @Bean JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter()); return jwtDecoder; } ``` Kotlin ``` @Bean fun jwtDecoder(): JwtDecoder { val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter()) return jwtDecoder } ``` ## 配置超时 默认情况下,Resource Server使用30秒的连接和套接字超时来与授权服务器进行协调。 在某些情况下,这可能太短了。此外,它没有考虑到更复杂的模式,比如后退和发现。 要调整资源服务器连接到授权服务器的方式,`NimbusJwtDecoder`接受`RestOperations`的实例: Java ``` @Bean public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { RestOperations rest = builder .setConnectTimeout(Duration.ofSeconds(60)) .setReadTimeout(Duration.ofSeconds(60)) .build(); NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); return jwtDecoder; } ``` Kotlin ``` @Bean fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder { val rest: RestOperations = builder .setConnectTimeout(Duration.ofSeconds(60)) .setReadTimeout(Duration.ofSeconds(60)) .build() return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build() } ``` 另外,默认情况下,Resource Server会在内存中缓存授权服务器的JWK设置5分钟,你可能需要对其进行调整。此外,它没有考虑更复杂的缓存模式,比如驱逐或使用共享缓存。 要调整资源服务器缓存JWK集的方式,`NimbusJwtDecoder`接受`Cache`的实例: Java ``` @Bean public JwtDecoder jwtDecoder(CacheManager cacheManager) { return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) .cache(cacheManager.getCache("jwks")) .build(); } ``` Kotlin ``` @Bean fun jwtDecoder(cacheManager: CacheManager): JwtDecoder { return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) .cache(cacheManager.getCache("jwks")) .build() } ``` 当给定一个`Cache`时,资源服务器将使用JWK设置的URI作为键,并使用JWK设置的JSON作为值。 | |Spring 不是缓存提供程序,因此你需要确保包含适当的依赖项,比如`spring-boot-starter-cache`和你最喜欢的缓存提供程序。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |不管是套接字超时还是缓存超时,你可能希望直接使用Nimbus。要做到这一点,请记住
附带一个构造函数,该构造函数接受Nimbus的`JWTProcessor`。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| [OAuth2资源服务器](index.html)[不透明令牌](opaque-token.html)