1. OAuth 2.0 Resource Server
Spring Security支持使用JWT编码的OAuth 2.0承载token保护端点。
应用程序将其权限管理联合到授权服务器(例如,Okta或Ping Identity)的情况下,这很方便。 资源服务器可以查询此授权服务器,以便在提供请求时验证权限。
可以在OAuth 2.0 Resource Server Servlet示例中找到完整的工作示例。
1.1 Dependencies
大多数资源服务器支持都收集到spring-security-oauth2-resource-server中。 但是,对解码和验证JWT的支持是spring-security-oauth2-jose,这意味着为了拥有支持JWT编码的承载token的工作资源服务器,两者都是必需的。
1.2 Minimal Configuration
使用Spring Boot时,将应用程序配置为资源服务器包含两个基本步骤。 首先,包括所需的依赖项,然后指出授权服务器的位置。
1.2.1 指定授权服务器
要指定要使用的授权服务器,只需执行以下操作:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
其中https://idp.example.com 是授权服务器将颁发的JWT令牌的iss声明中包含的值。 资源服务器将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的JWT。
要使用issuer-uri属性,https://idp.example.com/.well-known/openid-configuration必须是授权服务器支持的端点。此端点称为提供者配置端点。
1.2.2 启动期望
使用此属性和这些依赖项时,Resource Server将自动配置自身以验证JWT编码的承载token。
它通过确定性的启动过程实现了这一点:
- 点击Provider Configuration端点,https://idp.example.com/.well-known/openid-configuration,处理jwks_url属性的响应
- 配置验证策略以查询jwks_url以获取有效的公钥
- 配置验证策略以针对https://idp.example.com验证每个JWTs iss声明。
此过程的结果是授权服务器必须启动并接收请求才能使Resource Server成功启动。
如果授权服务器在资源服务器查询时关闭(给定适当的超时),则启动将失败。
1.2.3 运行期望
启动应用程序后,Resource Server将尝试处理包含Authorization:Bearer标头的任何请求:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示此方案,资源服务器将尝试根据承载令牌规范处理请求。
给定格式良好的JWT token,资源服务器将
- 根据在启动期间从jwks_url端点获取的公钥验证其签名,并与JWTs头匹配
- 验证JWTs exp和nbf时间戳以及JWTs iss声明,以及
- 将每个范围映射到具有前缀SCOPE_的权限。
当授权服务器提供新密钥时,Spring Security将自动轮换用于验证JWT令牌的密钥。
默认情况下,生成的Authentication#getPrincipal是Spring Security Jwt对象,Authentication#getName映射到JWT的子属性(如果存在)。
1.3 指定授权服务器JWK直接设置Uri
如果授权服务器不支持Provider Configuration端点,或者Resource Server必须能够独立于授权服务器启动,则可以将issuer-uri交换为jwk-set-uri:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
JWK Set uri不是标准化的,但通常可以在授权服务器的文档中找到
因此,资源服务器不会在启动时ping授权服务器。 但是,它也将不再验证JWT中的iss声明(因为资源服务器不再知道发行者的值应该是什么)。
此属性也可以直接在DSL上提供。
1.4 覆盖或替换引导自动配置
Spring Boot代表两个@Bean,它们代表资源服务器生成。
第一个是WebSecurityConfigurerAdapter,它将应用程序配置为资源服务器:
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
如果应用程序未公开WebSecurityConfigurerAdapter bean,则Spring Boot将公开上述默认值。
替换它就像在应用程序中公开bean一样简单:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(myConverter());
}
}
以上内容需要消息的范围:读取以/ messages /开头的任何URL。
oauth2ResourceServer DSL上的方法也将覆盖或替换自动配置。
例如,第二个@Bean Spring Boot创建的是一个JwtDecoder,它将String标记解码为Jwt的验证实例:
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromOidcIssuerLocation(issuerUri);
}
如果应用程序没有公开JwtDecoder bean,那么Spring Boot将公开上面的默认bean。
并且可以使用jwkSetUri()覆盖其配置或使用decoder()替换它的配置。
使用 jwkSetUri()
授权服务器的JWK Set Uri可以配置为配置属性,也可以在DSL中提供:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
}
}
使用jwkSetUri()优先于任何配置属性。
使用 decoder()
比jwkSetUri()更强大的是decoder(),它将完全取代JwtDecoder的任何Boot自动配置:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.decoder(myCustomDecoder());
}
}
当需要更深层次的配置(如验证,映射或请求超时)时,这很方便。
公开JwtDecoder @Bean
或者,公开JwtDecoder @Bean与decoder()具有相同的效果:
@Bean
public JwtDecoder jwtDecoder() {
return new NimbusJwtDecoderJwkSupport(jwkSetUri);
}
1.5 配置授权
从OAuth 2.0授权服务器发出的JWT通常具有范围或scp属性,指示已授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些范围强制转换为已授权的权限列表,并在每个范围前添加字符串“SCOPE_”。
这意味着要使用从JWT派生的作用域保护端点或方法,相应的表达式应包含此前缀:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
或者与方法安全性类似:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
手动提取权限
但是,在许多情况下,此默认值不足。 例如,某些授权服务器不使用scope属性,而是拥有自己的自定义属性。 或者,在其他时候,资源服务器可能需要使属性或属性的组合适应内部化的权限。
为此,DSL公开了jwtAuthenticationConverter():
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
}
}
Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
return new GrantedAuthoritiesExtractor();
}
负责将Jwt转换为身份验证。
我们可以简单地重写这一点来改变授予权限的方式:
static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {
protected Collection<GrantedAuthorities> extractAuthorities(Jwt jwt) {
Collection<String> authorities = (Collection<String>)
jwt.getClaims().get("mycustomclaim");
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
为了获得更大的灵活性,DSL支持完全用任何实现Converter <Jwt,AbstractAuthenticationToken>的类替换转换器:
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return new CustomAuthenticationToken(jwt);
}
}
1.6 配置验证
使用最小的Spring Boot配置,指示授权服务器的颁发者uri,Resource Server将默认验证iss声明以及exp和nbf时间戳声明。
在需要自定义验证的情况下,Resource Server附带两个标准验证器,并且还接受自定义OAuth2TokenValidator实例。
自定义时间戳验证
JWT通常有一个有效窗口,nbf索赔中指示的窗口的开始和exp索赔中指示的结尾。
但是,每个服务器都可能遇到时钟漂移,这可能导致令牌过期到一个服务器,但不会到另一个服务器。 随着协作服务器数量在分布式系统中的增加,这可能会导致一些实施灼伤。
资源服务器使用JwtTimestampValidator来验证令牌的有效性窗口,并且可以使用clockSkew配置它以缓解上述问题:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)
JwtDecoders.withOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new IssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
默认情况下,资源服务器配置30秒的时钟偏差。
配置自定义验证程序
使用OAuth2TokenValidator API添加对aud声明的检查很简单:
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
然后,要添加到资源服务器,需要指定JwtDecoder实例:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)
JwtDecoders.withOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
1.7 配置声明集映射
Spring Security使用Nimbus库来解析JWT并验证其签名。 因此,Spring Security受Nimbus对每个字段值的解释以及如何将每个字段强制转换为Java类型。
例如,因为Nimbus与Java 7兼容,所以它不使用Instant来表示时间戳字段。
并且完全可以使用不同的库或JWT处理,这可能会使自己的强制决策需要调整。
或者,很简单,资源服务器可能希望根据特定域的原因添加或删除JWT中的声明。
出于这些目的,Resource Server支持使用MappedJwtClaimSetConverter映射JWT声明集。
自定义单个声明的转换
默认情况下,MappedJwtClaimSetConverter
将尝试将声明强制转换为以下类型:
要求 | Java类型 |
---|---|
aud | Collection |
exp | Instant |
iat | Instant |
iss | String |
jti | String |
nbf | Instant |
sub | String |
可以使用MappedJwtClaimSetConverter.withDefaults配置单个声明的转换策略:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
jwtDecoder.setJwtClaimSetConverter(converter);
return jwtDecoder;
}
这将保留所有默认值,但它将覆盖sub的默认声明转换器。
添加声明
MappedJwtClaimSetConverter还可用于添加自定义声明,例如,以适应现有系统:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
删除声明
使用相同的API删除声明也很简单:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
重命名声明
在更复杂的场景中,例如一次查询多个声明或重命名声明,Resource Server接受任何实现Converter <Map <String,Object>,Map <String,Object >>的类:
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;
}
}
然后,可以像平常一样提供实例:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);
jwtDecoder.setJwtClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
1.8 配置超时
默认情况下,Resource Server使用每个30秒的连接和套接字超时来协调授权服务器。
在某些情况下,这可能太短。 此外,它没有考虑更复杂的模式,如退避和发现。
要调整Resource Server连接到授权服务器的方式,NimbusJwtDecoderJwkSupport接受RestOperations的实例:
@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
RestOperations rest = builder
.setConnectionTimeout(60000)
.setReadTimeout(60000)
.build();
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);
jwtDecoder.setRestOperations(rest);
return jwtDecoder;
}