OAuth 2.0 资源服务器 JWT

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 token 中 iss 声明的值。资源服务器将使用此属性进一步进行自我配置,发现授权服务器的公钥,并随后验证传入的 JWT。

要使用 issuer-uri 属性,还必须满足以下条件之一:idp.example.com/issuer/.well-known/openid-configurationidp.example.com/.well-known/openid-configuration/issueridp.example.com/.well-known/oauth-authorization-server/issuer 是授权服务器支持的端点。此端点被称为 提供程序配置 端点或 授权服务器元数据 端点。

就是这样!

启动预期

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

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

  1. 查询提供程序配置或授权服务器元数据端点以获取 jwks_url 属性

  2. 查询 jwks_url 端点以获取支持的算法

  3. 配置验证策略以查询 jwks_url 以获取发现的算法的有效公钥

  4. 配置验证策略以根据 idp.example.com 验证每个 JWT 的 iss 声明。

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

如果授权服务器在资源服务器查询它时宕机(在适当的超时时间内),则启动将失败。

运行时预期

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

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

只要指示了此方案,资源服务器将尝试根据 Bearer Token 规范处理请求。

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

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

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

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

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

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

从这里,考虑跳转到

JWT 认证如何工作

接下来,让我们看看 Spring Security 用于支持 servlet 应用程序中的 JWT 认证的架构组件,就像我们刚刚看到的那样。

让我们看看 JwtAuthenticationProvider 如何在 Spring Security 中工作。该图解释了 读取 Bearer Token 中的图中的 AuthenticationManager 如何工作的详细信息。

jwtauthenticationprovider
图 1. JwtAuthenticationProvider 用法

数字 1 读取 Bearer Token 中的认证 FilterBearerTokenAuthenticationToken 传递给由 ProviderManager 实现的 AuthenticationManager

数字 2 ProviderManager 配置为使用类型为 JwtAuthenticationProviderAuthenticationProvider

数字 3 JwtAuthenticationProvider 使用 JwtDecoder 解码、验证和校验 Jwt

数字 4 JwtAuthenticationProvider 然后使用 JwtAuthenticationConverterJwt 转换为授予权限的 Collection

数字 5 认证成功时,返回的 Authentication 类型为 JwtAuthenticationToken,其 principal 是由配置的 JwtDecoder 返回的 Jwt,并且包含至少 FACTOR_BEARER 的一组权限。最终,返回的 JwtAuthenticationToken 将由认证 Filter 设置在 SecurityContextHolder 上。

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

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

提供受众

如前所述,issuer-uri 属性验证 iss 声明;这是发送 JWT 的人。

Boot 还有 audiences 属性用于验证 aud 声明;这是 JWT 发送给谁。

资源服务器的受众可以这样表示

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          audiences: https://my-resource-server.example.com
如果需要,您还可以以编程方式添加 aud 验证

结果将是,如果 JWT 的 iss 声明不是 idp.example.com,并且其 aud 声明不包含 my-resource-server.example.com 在其列表中,则验证将失败。

覆盖或替换 Boot 自动配置

Spring Boot 会为资源服务器生成两个 @Bean

第一个是 SecurityFilterChain,它将应用程序配置为资源服务器。当包含 spring-security-oauth2-jose 时,此 SecurityFilterChain 如下所示

默认 JWT 配置
  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
    return http.build()
}

如果应用程序没有公开 SecurityFilterChain bean,那么 Spring Boot 将公开上述默认 bean。

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

自定义 JWT 配置
  • Java

  • Kotlin

import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/messages/**").access(hasScope("message:read"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .jwtAuthenticationConverter(myConverter())
                )
            );
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope

@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/messages/**", hasScope("message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = myConverter()
                }
            }
        }
        return http.build()
    }
}

上述要求以 /messages/ 开头的任何 URL 都具有 message:read 范围。

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

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

JWT 解码器
  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return JwtDecoders.fromIssuerLocation(issuerUri)
}
调用 JwtDecoders#fromIssuerLocation 是为了调用 Provider Configuration 或 Authorization Server Metadata 端点,以获取 JWK Set Uri。

如果应用程序没有公开 JwtDecoder bean,那么 Spring Boot 将公开上述默认 bean。

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

或者,如果您根本不使用 Spring Boot,那么这两个组件——过滤器链和 JwtDecoder 可以在 XML 中指定。

过滤器链的指定方式如下

默认 JWT 配置
  • Xml

<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="jwtDecoder"/>
    </oauth2-resource-server>
</http>

JwtDecoder 的指定方式如下

JWT 解码器
  • Xml

<bean id="jwtDecoder"
        class="org.springframework.security.oauth2.jwt.JwtDecoders"
        factory-method="fromIssuerLocation">
    <constructor-arg value="${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}"/>
</bean>

使用 jwkSetUri()

授权服务器的 JWK Set Uri 可以作为配置属性配置,也可以在 DSL 中提供

JWK Set Uri 配置
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
                )
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
                }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.com/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

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

使用 decoder()

jwkSetUri() 更强大的是 decoder(),它将完全替换任何 Boot 自动配置的 JwtDecoder

JWT 解码器配置
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .decoder(myCustomDecoder())
                )
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = myCustomDecoder()
                }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="myCustomDecoder"/>
    </oauth2-resource-server>
</http>

当需要更深层次的配置,例如验证映射请求超时时,这非常方便。

公开 JwtDecoder @Bean

或者,公开一个 JwtDecoder @Bean 具有与 decoder() 相同的效果。您可以使用 jwkSetUri 构造一个,如下所示

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}

或者您可以使用发行者,并让 NimbusJwtDecoder 在调用 build() 时查找 jwkSetUri,如下所示

  • Java

  • Kotlin

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

或者,如果默认值适合您,您也可以使用 JwtDecoders,它除了配置解码器的验证器外,还执行上述操作

  • Java

  • Kotlin

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

配置受信任的算法

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

您可以通过 Spring BootNimbusJwtDecoder 构建器或从 JWK Set 响应自定义此设置。

通过 Spring Boot

设置算法最简单的方法是将其作为属性

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

使用构建器

然而,为了获得更大的能力,我们可以使用 NimbusJwtDecoder 附带的构建器

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).build()
}

多次调用 jwsAlgorithm 将配置 NimbusJwtDecoder 信任多种算法,如下所示

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}

或者,您可以调用 jwsAlgorithms

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithms(algorithms -> {
                    algorithms.add(RS512);
                    algorithms.add(ES512);
            }).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithms {
                it.add(RS512)
                it.add(ES512)
            }.build()
}

从 JWK Set 响应

由于 Spring Security 的 JWT 支持是基于 Nimbus 的,因此您也可以使用它的所有强大功能。

例如,Nimbus 有一个 JWSKeySelector 实现,它将根据 JWK Set URI 响应选择算法集。您可以这样使用它来生成 NimbusJwtDecoder

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    // makes a request to the JWK Set endpoint
    JWSKeySelector<SecurityContext> jwsKeySelector =
            JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);

    DefaultJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor<>();
    jwtProcessor.setJWSKeySelector(jwsKeySelector);

    return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    // makes a request to the JWK Set endpoint
    val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
    val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
    jwtProcessor.jwsKeySelector = jwsKeySelector
    return NimbusJwtDecoder(jwtProcessor)
}

信任单个非对称密钥

比使用 JWK Set 端点支持资源服务器更简单的是硬编码 RSA 公钥。公钥可以通过 Spring Boot 或通过使用构建器提供。

通过 Spring Boot

通过 Spring Boot 指定密钥非常简单。密钥的位置可以这样指定

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

或者,为了实现更复杂的查找,您可以对 RsaKeyConversionServicePostProcessor 进行后处理

  • Java

  • Kotlin

@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
    return beanFactory ->
        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
                .setResourceLoader(new CustomResourceLoader());
}
@Bean
fun conversionServiceCustomizer(): BeanFactoryPostProcessor {
    return BeanFactoryPostProcessor { beanFactory ->
        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

使用构建器

要直接连接 RSAPublicKey,您只需使用相应的 NimbusJwtDecoder 构建器,如下所示

  • Java

  • Kotlin

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

信任单个对称密钥

使用单个对称密钥也很简单。您只需加载 SecretKey 并使用适当的 NimbusJwtDecoder 构建器,如下所示

  • Java

  • Kotlin

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

配置授权

从 OAuth 2.0 授权服务器颁发的 JWT 通常会有一个 scopescp 属性,指示其已授予的范围(或权限),例如

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

在这种情况下,资源服务器将尝试将这些作用域强制转换为一个授予权限列表,每个作用域都带有前缀 "SCOPE_"。

这意味着要使用从 JWT 派生的作用域保护端点或方法,相应的表达式应包含此前缀

授权配置
  • Java

  • Kotlin

  • Xml

import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/contacts/**").access(hasScope("contacts"))
                .requestMatchers("/messages/**").access(hasScope("messages"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/contacts/**", hasScope("contacts"))
                authorize("/messages/**", hasScope("messages"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt { }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

或者与方法安全类似

  • Java

  • Kotlin

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

手动提取权限

然而,在许多情况下,此默认值是不够的。例如,某些授权服务器不使用 scope 属性,而是使用自己的自定义属性。或者,有时,资源服务器可能需要将属性或属性组合转换为内部权限。

为此,Spring Security 附带 JwtAuthenticationConverter,它负责 Jwt 转换为 Authentication。默认情况下,Spring Security 将使用 JwtAuthenticationConverter 的默认实例连接 JwtAuthenticationProvider

作为配置 JwtAuthenticationConverter 的一部分,您可以提供一个辅助转换器,用于将 Jwt 转换为授予权限的 Collection

假设您的授权服务器在名为 authorities 的自定义声明中通信权限。在这种情况下,您可以配置 JwtAuthenticationConverter 应该检查的声明,如下所示

权限声明配置
  • Java

  • Kotlin

  • Xml

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities")

    val jwtAuthenticationConverter = JwtAuthenticationConverter()
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
    return jwtAuthenticationConverter
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authoritiesClaimName" value="authorities"/>
</bean>

您还可以将权限前缀配置为不同。不是为每个权限都加上 SCOPE_ 前缀,您可以将其更改为 ROLE_,如下所示

权限前缀配置
  • Java

  • Kotlin

  • Xml

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")

    val jwtAuthenticationConverter = JwtAuthenticationConverter()
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
    return jwtAuthenticationConverter
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authorityPrefix" value="ROLE_"/>
</bean>

或者,您可以通过调用 JwtGrantedAuthoritiesConverter#setAuthorityPrefix("") 完全移除前缀。

为了更大的灵活性,DSL 支持完全用实现 Converter<Jwt, AbstractAuthenticationToken> 的任何类替换转换器

  • Java

  • Kotlin

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return new CustomAuthenticationToken(jwt);
    }
}

// ...

@Configuration
@EnableWebSecurity
public class CustomAuthenticationConverterConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt((jwt) -> jwt
                    .jwtAuthenticationConverter(new CustomAuthenticationConverter())
                )
            );
        return http.build();
    }
}
internal class CustomAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
    override fun convert(jwt: Jwt): AbstractAuthenticationToken {
        return CustomAuthenticationToken(jwt)
    }
}

// ...

@Configuration
@EnableWebSecurity
class CustomAuthenticationConverterConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
       http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
           oauth2ResourceServer {
               jwt {
                   jwtAuthenticationConverter = CustomAuthenticationConverter()
               }
           }
        }
        return http.build()
    }
}

配置验证

使用最少的 Spring Boot 配置,指示授权服务器的发行者 URI,资源服务器将默认验证 iss 声明以及 expnbf 时间戳声明。

在需要自定义验证的情况下,资源服务器附带两个标准验证器,也接受自定义的 OAuth2TokenValidator 实例。

自定义时间戳验证

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

然而,每个服务器都可能出现时钟漂移,这可能导致令牌在一个服务器上看起来已过期,但在另一个服务器上却没有。在分布式系统中,随着协作服务器数量的增加,这可能会导致一些实现上的麻烦。

资源服务器使用 JwtTimestampValidator 来验证令牌的有效期窗口,并且可以配置 clockSkew 以缓解上述问题

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
     NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
             JwtDecoders.fromIssuerLocation(issuerUri);

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new JwtIssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder

    val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
            JwtTimestampValidator(Duration.ofSeconds(60)),
            JwtIssuerValidator(issuerUri))

    jwtDecoder.setJwtValidator(withClockSkew)

    return jwtDecoder
}
默认情况下,资源服务器配置的时钟偏差为 60 秒。

配置 RFC 9068 验证

如果您需要要求满足 RFC 9068 的令牌,您可以通过以下方式配置验证

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
            .validateTypes(false).build();
    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
            .audience("https://audience.example.org")
            .clientId("client-identifier")
            .issuer("https://issuer.example.org").build());
     return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri)
            .validateTypes(false).build()
    jwtDecoder.setJwtValidator(JwtValidators.createAtJwtValidator()
            .audience("https://audience.example.org")
            .clientId("client-identifier")
            .issuer("https://issuer.example.org").build())
    return jwtDecoder
}

配置自定义验证器

使用 OAuth2TokenValidator API 添加对 aud 声明的检查非常简单

  • Java

  • Kotlin

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
fun audienceValidator(): OAuth2TokenValidator<Jwt?> {
    return JwtClaimValidator<List<String>>(AUD) { aud -> aud.contains("messaging") }
}

或者,为了获得更多控制,您可以实现自己的 OAuth2TokenValidator

  • Java

  • Kotlin

static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    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<Jwt> audienceValidator() {
    return new AudienceValidator();
}
internal class AudienceValidator : OAuth2TokenValidator<Jwt> {
    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<Jwt> {
    return AudienceValidator()
}

然后,要将其添加到资源服务器中,只需指定 JwtDecoder 实例

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder

    val audienceValidator = audienceValidator()
    val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
    val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)

    jwtDecoder.setJwtValidator(withAudience)

    return jwtDecoder
}
如前所述,您可以转而在 Boot 中配置 aud 验证

配置声明集映射

Spring Security 使用 Nimbus 库来解析 JWT 并验证其签名。因此,Spring Security 受 Nimbus 对每个字段值的解释以及如何将每个字段强制转换为 Java 类型的影响。

例如,由于 Nimbus 仍然兼容 Java 7,它不使用 Instant 来表示时间戳字段。

而且,完全有可能使用不同的库进行 JWT 处理,这可能会做出需要调整的自己的强制转换决策。

或者,很简单,资源服务器可能希望出于特定领域的原因添加或删除 JWT 中的声明。

出于这些目的,资源服务器支持使用 MappedJwtClaimSetConverter 映射 JWT 声明集。

自定义单个声明的转换

默认情况下,MappedJwtClaimSetConverter 将尝试将声明强制转换为以下类型

声明

Java 类型

aud

Collection<String>

exp

Instant

iat

Instant

iss

字符串

jti

字符串

nbf

Instant

sub

字符串

可以使用 MappedJwtClaimSetConverter.withDefaults 配置单个声明的转换策略

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();

    MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
            .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
    jwtDecoder.setClaimSetConverter(converter);

    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()

    val converter = MappedJwtClaimSetConverter
            .withDefaults(mapOf("sub" to this::lookupUserIdBySub))
    jwtDecoder.setClaimSetConverter(converter)

    return jwtDecoder
}

这将保留所有默认值,但会覆盖 sub 的默认声明转换器。

添加声明

MappedJwtClaimSetConverter 也可以用于添加自定义声明,例如,以适应现有系统

  • Java

  • Kotlin

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))

移除声明

使用相同的 API,删除声明也很简单

  • Java

  • Kotlin

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))

重命名声明

在更复杂的场景中,例如同时查询多个声明或重命名声明,资源服务器接受任何实现 Converter<Map<String, Object>, Map<String,Object>> 的类

  • Java

  • Kotlin

public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);

        String username = (String) convertedClaims.get("user_name");
        convertedClaims.put("sub", username);

        return convertedClaims;
    }
}
class UsernameSubClaimAdapter : Converter<Map<String, Any?>, Map<String, Any?>> {
    private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap())
    override fun convert(claims: Map<String, Any?>): Map<String, Any?> {
        val convertedClaims = delegate.convert(claims)
        val username = convertedClaims["user_name"] as String
        convertedClaims["sub"] = username
        return convertedClaims
    }
}

然后,实例可以像往常一样提供

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
    jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
    jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
    return jwtDecoder
}

配置超时

默认情况下,资源服务器与授权服务器协调时,连接和套接字超时各为 30 秒。

在某些情况下,这可能太短了。此外,它没有考虑更复杂的模式,如退避和发现。

为了调整资源服务器连接授权服务器的方式,NimbusJwtDecoder 接受一个 RestOperations 实例

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build();

    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build();
    return jwtDecoder;
}
@Bean
fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
    val rest: RestOperations = builder
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build()
    return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build()
}

此外,默认情况下,资源服务器将授权服务器的 JWK 集在内存中缓存 5 分钟,您可能需要调整此时间。此外,它没有考虑更复杂的缓存模式,如逐出或使用共享缓存。

为了调整资源服务器缓存 JWK 集的方式,NimbusJwtDecoder 接受一个 Cache 实例

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
    return NimbusJwtDecoder.withIssuerLocation(issuer)
            .cache(cacheManager.getCache("jwks"))
            .build();
}
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(issuer)
            .cache(cacheManager.getCache("jwks"))
            .build()
}

当给定一个 Cache 时,资源服务器将使用 JWK Set Uri 作为键,JWK Set JSON 作为值。

Spring 不是缓存提供商,所以您需要确保包含适当的依赖项,如 spring-boot-starter-cache 和您喜欢的缓存提供商。
无论是套接字还是缓存超时,您可能都希望直接使用 Nimbus。为此,请记住 NimbusJwtDecoder 附带一个接受 Nimbus 的 JWTProcessor 的构造函数。
© . This site is unofficial and not affiliated with VMware.