9.Spring Security 5.1之 OAuth 2.0 Resource Server

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。

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

此过程的结果是授权服务器必须启动并接收请求才能使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类型
audCollection
expInstant
iatInstant
issString
jtiString
nbfInstant
subString

可以使用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;
}
技术宅星云 CSDN认证博客专家 Java MySQL Redis
技术宅星云(网名),英文名fairy,先后曾在惠普,北京中国航信工作, 目前担任北京蛙跳科技有限公司后端高级开发工程师,负责公司短视频App应用后台,擅长JAVA后端技术,CSDN博客专家。
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页
实付 59.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值