对于最新的稳定版本,请使用 Spring Security 6.5.3spring-doc.cadn.net.cn

OAuth 2.0 资源服务器 JWT

JWT 的最小依赖关系

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

JWT 的最小配置

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

指定授权服务器

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

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

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

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

创业期望

使用此属性和这些依赖项时,资源服务器将自动配置自身以验证 JWT 编码的持有者Tokens。spring-doc.cadn.net.cn

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

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

  2. 查询jwks_url支持算法的端点spring-doc.cadn.net.cn

  3. 配置验证策略以查询jwks_url找到的算法的有效公键spring-doc.cadn.net.cn

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

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

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

运行时预期

应用程序启动后,资源服务器将尝试处理包含Authorization: Bearer页眉:spring-doc.cadn.net.cn

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

只要指示此方案,资源服务器就会尝试根据持有者Tokens规范处理请求。spring-doc.cadn.net.cn

给定格式正确的 JWT,资源服务器将:spring-doc.cadn.net.cn

  1. 根据从jwks_url端点,并与 JWT 匹配spring-doc.cadn.net.cn

  2. 验证 JWT 的expnbf时间戳和 JWTissclaim 和spring-doc.cadn.net.cn

  3. 将每个作用域映射到具有前缀SCOPE_.spring-doc.cadn.net.cn

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

由此产生的Authentication#getPrincipal,默认情况下是 Spring SecurityJwt对象,以及Authentication#getName映射到 JWT 的sub属性,如果存在的话。spring-doc.cadn.net.cn

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

JWT 身份验证的工作原理

接下来,让我们看看 Spring Security 用于在基于 servlet 的应用程序中支持 JWT 身份验证的架构组件,就像我们刚才看到的那个。spring-doc.cadn.net.cn

让我们来看看如何JwtAuthenticationProvider在 Spring Security 中工作。该图解释了如何AuthenticationManager《阅读不记名Tokens》作品中的数字中。spring-doc.cadn.net.cn

jwt身份验证提供程序
图 1.JwtAuthenticationProvider用法

1号身份验证Filter读取不记名Tokens传递一个BearerTokenAuthenticationTokenAuthenticationManagerProviderManager.spring-doc.cadn.net.cn

2号ProviderManager配置为使用 AuthenticationProvider 类型JwtAuthenticationProvider.spring-doc.cadn.net.cn

3号 JwtAuthenticationProvider解码、验证和验证Jwt使用JwtDecoder.spring-doc.cadn.net.cn

4号 JwtAuthenticationProvider然后使用JwtAuthenticationConverterJwt变成一个Collection授予的权力。spring-doc.cadn.net.cn

5号身份验证成功后,Authentication返回的 is 类型为JwtAuthenticationToken并且有一个主体,即Jwt由配置的JwtDecoder. 最终,返回的JwtAuthenticationToken将设置在SecurityContextHolder通过身份验证Filter.spring-doc.cadn.net.cn

直接指定授权服务器 JWK 设置 URI

如果授权服务器不支持任何配置端点,或者资源服务器必须能够独立于授权服务器启动,则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 Set uri 不是标准化的,但通常可以在授权服务器的文档中找到

因此,资源服务器不会在启动时 ping 授权服务器。 我们仍然指定issuer-uri以便资源服务器仍会验证iss对传入 JWT 的声明。spring-doc.cadn.net.cn

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

覆盖或替换引导自动配置

有两个@BeanSpring Boot 代表资源服务器生成的。spring-doc.cadn.net.cn

第一个是SecurityFilterChain将应用配置为资源服务器。当包括spring-security-oauth2-joseSecurityFilterChain看来:spring-doc.cadn.net.cn

默认 JWT 配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
    return http.build()
}

如果应用程序未公开SecurityFilterChainbean,那么 Spring Boot 将公开上述默认的。spring-doc.cadn.net.cn

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

自定义 JWT 配置
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(myConverter())
                )
            );
        return http.build();
    }
}
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("SCOPE_message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = myConverter()
                }
            }
        }
        return http.build()
    }
}

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

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

例如,第二个@BeanSpring Boot 创建的是一个JwtDecoder解码StringTokens转换为Jwt:spring-doc.cadn.net.cn

JWT 解码器
@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return JwtDecoders.fromIssuerLocation(issuerUri)
}
JwtDecoders#fromIssuerLocation调用提供程序配置或授权服务器元数据端点以派生 JWK 集 URI。

如果应用程序未公开JwtDecoderbean,那么 Spring Boot 将公开上述默认的。spring-doc.cadn.net.cn

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

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

过滤器链的指定如下:spring-doc.cadn.net.cn

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

以及JwtDecoder这样:spring-doc.cadn.net.cn

JWT 解码器
<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 中提供:spring-doc.cadn.net.cn

JWK 设置 URI 配置
@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();
    }
}
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                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()优先于任何配置属性。spring-doc.cadn.net.cn

decoder()

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

JWT 解码器配置
@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();
    }
}
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                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>

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

公开JwtDecoder @Bean

或者,公开JwtDecoder @Bean具有与decoder():spring-doc.cadn.net.cn

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

配置可信算法

默认情况下,NimbusJwtDecoder,因此资源服务器将仅信任和验证使用RS256.spring-doc.cadn.net.cn

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

通过 Spring Boot

设置算法的最简单方法是作为属性:spring-doc.cadn.net.cn

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

使用构建器

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

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

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

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

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

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

从 JWK Set 响应

由于 Spring Security 的 JWT 支持基于 Nimbus,因此您也可以使用它的所有强大功能。spring-doc.cadn.net.cn

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

@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 集端点支持资源服务器更简单的方法是对 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

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

使用构建器

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

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

信任单个对称密钥

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

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

配置授权

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

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

在这种情况下,资源服务器将尝试将这些作用域强制到已授予权限的列表中,并在每个作用域前面加上字符串“SCOPE_”。spring-doc.cadn.net.cn

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

授权配置
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }
}
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
                authorize("/messages/**", hasAuthority("SCOPE_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>

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

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

手动提取权限

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

为此,Spring Security 附带了JwtAuthenticationConverter,负责Jwt变成一个Authentication. 默认情况下,Spring Security 将连接JwtAuthenticationProvider默认实例为JwtAuthenticationConverter.spring-doc.cadn.net.cn

作为配置JwtAuthenticationConverter,您可以提供一个辅助转换器Jwt设置为Collection授予的权力。spring-doc.cadn.net.cn

假设您的授权服务器在名为authorities. 在这种情况下,可以配置声明JwtAuthenticationConverter应该检查,如下所示:spring-doc.cadn.net.cn

当局声明配置
@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_这样:spring-doc.cadn.net.cn

权限前缀配置
@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("").spring-doc.cadn.net.cn

为了获得更大的灵活性,DSL 支持将转换器完全替换为实现Converter<Jwt, AbstractAuthenticationToken>:spring-doc.cadn.net.cn

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

// ...

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

// ...

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

配置验证

使用最少的 Spring Boot 配置,指示授权服务器的颁发者 uri,资源服务器将默认验证issclaim 以及expnbftimestamp 声明。spring-doc.cadn.net.cn

在需要自定义验证的情况下,资源服务器附带两个标准验证器,并且还接受自定义OAuth2TokenValidator实例。spring-doc.cadn.net.cn

自定义时间戳验证

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

但是,每台服务器都可能遇到时钟漂移,这可能导致Tokens在一台服务器上显示为过期,但对另一台服务器则不然。 随着分布式系统中协作服务器数量的增加,这可能会导致一些实现胃灼热。spring-doc.cadn.net.cn

资源服务器使用JwtTimestampValidator以验证Tokens的有效性窗口,并且可以将其配置为clockSkew为了缓解上述问题:spring-doc.cadn.net.cn

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

配置自定义验证器

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

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

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

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

配置声明集映射

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

例如,由于 Nimbus 仍然与 Java 7 兼容,因此它不使用Instant来表示时间戳字段。spring-doc.cadn.net.cn

而且完全有可能使用不同的库或进行 JWT 处理,这可能会做出自己的强制决策,需要调整。spring-doc.cadn.net.cn

或者,很简单,资源服务器可能出于特定于域的原因想要在 JWT 中添加或删除声明。spring-doc.cadn.net.cn

出于这些目的,资源服务器支持将 JWT 声明集映射到MappedJwtClaimSetConverter.spring-doc.cadn.net.cn

自定义单个声明的转换

默认情况下,MappedJwtClaimSetConverter将尝试强制声明为以下类型:spring-doc.cadn.net.cn

索赔spring-doc.cadn.net.cn

Java 类型spring-doc.cadn.net.cn

audspring-doc.cadn.net.cn

Collection<String>spring-doc.cadn.net.cn

expspring-doc.cadn.net.cn

Instantspring-doc.cadn.net.cn

iatspring-doc.cadn.net.cn

Instantspring-doc.cadn.net.cn

issspring-doc.cadn.net.cn

Stringspring-doc.cadn.net.cn

jtispring-doc.cadn.net.cn

Stringspring-doc.cadn.net.cn

nbfspring-doc.cadn.net.cn

Instantspring-doc.cadn.net.cn

subspring-doc.cadn.net.cn

Stringspring-doc.cadn.net.cn

可以使用以下命令配置单个版权主张的转化策略MappedJwtClaimSetConverter.withDefaults:spring-doc.cadn.net.cn

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();

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

    return jwtDecoder;
}
@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.spring-doc.cadn.net.cn

添加声明

MappedJwtClaimSetConverter还可用于添加自定义声明,例如,适应现有系统:spring-doc.cadn.net.cn

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

删除声明

使用相同的 API 删除声明也很简单:spring-doc.cadn.net.cn

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

重命名声明

在更复杂的方案中,例如一次查阅多个声明或重命名声明,资源服务器接受实现Converter<Map<String, Object>, Map<String,Object>>:spring-doc.cadn.net.cn

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

然后,可以像往常一样提供实例:spring-doc.cadn.net.cn

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

配置超时

默认情况下,资源服务器使用连接超时和套接字超时(分别为 30 秒)来与授权服务器进行协调。spring-doc.cadn.net.cn

在某些情况下,这可能太短。 此外,它没有考虑更复杂的模式,例如退避和发现。spring-doc.cadn.net.cn

要调整资源服务器连接到授权服务器的方式,NimbusJwtDecoder接受RestOperations:spring-doc.cadn.net.cn

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

此外,默认情况下,资源服务器会在内存中缓存授权服务器的 JWK 集 5 分钟,您可能需要对其进行调整。 此外,它没有考虑更复杂的缓存模式,例如逐出或使用共享缓存。spring-doc.cadn.net.cn

要调整资源服务器缓存 JWK 集的方式,NimbusJwtDecoder接受Cache:spring-doc.cadn.net.cn

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

当给定一个Cache,资源服务器将使用 JWK Set Uri 作为键,使用 JWK Set JSON 作为值。spring-doc.cadn.net.cn

Spring 不是缓存提供程序,因此您需要确保包含适当的依赖项,例如spring-boot-starter-cache以及您最喜欢的缓存提供程序。
无论是套接字还是缓存超时,你都可能希望直接使用 Nimbus。 为此,请记住NimbusJwtDecoder带有一个采用 Nimbus 的JWTProcessor.