# OAuth2.0资源服务器JWT

# JWT的最小依赖项

大多数资源服务器支持被收集到spring-security-oauth2-resource-server中。然而,对JWTS的解码和验证的支持是spring-security-oauth2-jose,这意味着这两个都是必要的,以便拥有一个支持JWT编码的承载令牌的工作资源服务器。

# JWTS的最小配置

当使用Spring Boot (opens new window)时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。

# 指定授权服务器

在 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)中的一个是授权服务器所支持的端点。
此端点被称为提供者配置 (opens new window)端点或授权服务器元数据 (opens new window)端点。

就这样!

# 创业期望

当使用此属性和这些依赖项时,Resource Server将自动配置自身以验证JWT编码的承载令牌。

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

  1. 查询jwks_url属性的提供者配置或授权服务器元数据端点

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

  3. 将验证策略配置为查询jwks_url中找到的算法的有效公钥

  4. 将验证策略配置为针对[https://idp.example.com](https://idp.example.com)的每个JWTSiss索赔进行验证。

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

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

# 运行时期望

一旦启动应用程序,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的expnbf时间戳以及JWT的iss声明,并

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

由于授权服务器提供了可用的新密钥, Spring 安全性将自动旋转用于验证JWTS的密钥。

默认情况下,生成的Authentication#getPrincipal是 Spring securityJwt对象,并且Authentication#getName映射到JWT的sub属性(如果存在)。

从这里开始,考虑跳到:

# JWT身份验证的工作方式

接下来,让我们看看 Spring Security在基于 Servlet 的应用程序中支持JWT (opens new window)身份验证所使用的体系结构组件,就像我们刚才看到的那样。

[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)在读取不记名令牌中的工作原理。

JWTAutHenticationProvider

图1. JwtAuthenticationProvider用法

number 1来自读取不记名令牌的身份验证Filter将一个BearerTokenAuthenticationToken传递到AuthenticationManager,这是由[ProviderManager](.../../authentication/architecture.html# Servlet-authentication-providermanager)实现的。

number 2ProviderManager被配置为使用身份验证提供者类型的JwtAuthenticationProvider

number 3JwtAuthenticationProvider使用[JwtDecoder](#OAuth2Resourceserver-JWT-decoder)对Jwt进行解码、验证和验证。

number 4JwtAuthenticationProvider然后使用[JwtAuthenticationConverter](#OAuth2Resourceserver-JWT-Authorization-Extraction)将Jwt转换为授予权限的Collection

number 5当身份验证成功时,返回的[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上直接提供此属性。

# 覆盖或替换Boot Auto配置

有两个@Beans, 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的范围。

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

例如,第二个@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

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

JwtDecoder就像这样:

例5. 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>

# `

可以配置授权服务器的JWK集URI作为配置属性,也可以在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

<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()优先于任何配置属性。

# `

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

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

当需要更深的配置时,比如validationmapping请求超时,这是很方便的。

# 曝光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 BootNimbusJWTDecoder Builder或从JWK SET响应进行自定义。

# 通过 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<SecurityContext> jwsKeySelector =
            JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);

    DefaultJWTProcessor<SecurityContext> 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<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
    val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
    jwtProcessor.jwsKeySelector = jwsKeySelector
    return NimbusJwtDecoder(jwtProcessor)
}

# 信任单一的非对称密钥

比支持具有JWK设置端点的资源服务器更简单的方法是对RSA公钥进行硬编码。公钥可以通过Spring Boot使用构建器提供。

# 通过 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<RsaKeyConversionServicePostProcessor>()
                .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通常具有scopescp属性,指示已授予的范围(或权限),例如:

{ …​, "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

<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

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

Kotlin

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

# 手动提取权限

然而,在许多情况下,这种默认设置是不够的。例如,一些授权服务器不使用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

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

你也可以将权限前缀配置为不同的。你可以将它更改为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

<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

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    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<Jwt, AbstractAuthenticationToken> {
    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(表示授权服务器的发行者URI),Resource Server将默认验证iss声明以及expnbf时间戳声明。

在需要定制验证的情况下,Resource Server附带两个标准验证器,并且还接受定制的OAuth2TokenValidator实例。

# 自定义时间戳验证

JWT通常有一个有效窗口,窗口的开始在nbf索赔中指示,结束在exp索赔中指示。

然而,每个服务器都可能经历时钟漂移,这可能导致令牌在一台服务器上出现过期,而不是在另一台服务器上出现。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现令人心烦。

Resource Server使用JwtTimestampValidator来验证令牌的有效性窗口,并且可以将其配置为clockSkew以缓解上述问题:

Java

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

Kotlin

@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
}
默认情况下,Resource Server配置的时钟偏差为60秒。

# 配置自定义验证器

使用OAuth2TokenValidatorAPI添加aud声明的检查很简单:

Java

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

Kotlin

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

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

Java

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

Kotlin

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](#OAuth2Resourceserver-jwt-architecture-jwtdecoder)实例:

Java

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

Kotlin

@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 安全性使用Nimbus (opens new window)库来解析JWTS并验证其签名。因此, Spring 安全性取决于Nimbus对每个字段值的解释以及如何将每个字段值强制为 Java 类型。

例如,因为Nimbus保持 Java 7兼容,所以它不使用Instant来表示时间戳字段。

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

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

出于这些目的,Resource Server支持用MappedJwtClaimSetConverter映射JWT索赔集。

# 定制单个索赔的转换

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

Claim Java 类型
aud Collection<String>
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<Any, String> { "value" }))

# 删除索赔

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

Java

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));

Kotlin

MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))

# 重新命名索赔

在更复杂的场景中,例如一次查询多个声明或重命名一个声明,Resource Server接受实现Converter<Map<String, Object>, Map<String,Object>>的任何类:

Java

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

Kotlin

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

@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资源服务器不透明令牌