51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Authorization Server 中将 Authorities 作为自定义 Claim 添加到 JWT Access Token 中。

1、概览 {#1概览}

在许多情况下,可以在 JWT Access Token 添加自定义 Claim,从而在 Token Payload 中包含更多信息。

本文将带你了解如何在 Spring Authorization Server 中为 JWT Access Token 添加资源所有者授权。

2、Spring Authorization Server {#2spring-authorization-server}

Spring Authorization Server 是 Spring 生态系统中的一个新项目,旨在为 Spring 应用提供授权服务器支持。它通过 Spring 编程模型简化 OAuth 2.0 和 OpenID Connect(OIDC) 授权服务器的实现。

2.1、Maven 依赖 {#21maven-依赖}

首先,在 pom.xml 中导入 spring-boot-starter-webspring-boot-starter-securityspring-boot-starter-testspring-security-oauth2-authorization-server 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.5.4</version>
</dependency>

或者,可以在 pom.xml 文件中添加 spring-boot-starter-oauth2-authorization-server 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    <version>3.2.0</version>
</dependency>

2.2、项目设置 {#22项目设置}

配置 Spring Authorization Server 来签发 Access Token。

为了简单起见,这里使用 "Spring Security Oauth 授权服务器" 应用。

假设我们使用的是 GitHub 上的 授权服务器项目

3、为 JWT Access Token 添加基本的自定义 Claim {#3为-jwt-access-token-添加基本的自定义-claim}

在基于 Spring Security OAuth2 的应用中,可以通过自定义授权服务器中的令牌创建流程,为 JWT Access Token 添加自定义 Claim。这种类型的 Claim 可用于在 JWT 中注入附加信息,然后由资源服务器或身份认证和授权流程中的其他组件使用。

3.1、添加基本的自定义 Claim {#31添加基本的自定义-claim}

可以使用 OAuth2TokenCustomizer<JWTEncodingContext> Bean 将自定义 Claim 添加到 Access Token 中。通过使用它,授权服务器签发的每个 Access Token 都将填充自定义 Claim。

DefaultSecurityConfig 类中添加 OAuth2TokenCustomizer Bean:

@Bean
@Profile("basic-claim")
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
    return (context) -> {
      if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
        context.getClaims().claims((claims) -> {
          claims.put("claim-1", "value-1");
          claims.put("claim-2", "value-2");
        });
      }
    };
}

OAuth2TokenCustomizer 接口是 Spring Security OAuth2 库的一部分,用于自定义 OAuth 2.0 Token。在本例中,它专门用于在编码过程中自定义 JWT Token。

传递给 jwtTokenCustomizer() Bean 的 lambda 表达式定义了自定义逻辑。context 参数代表 Token 编码过程中的 JwtEncodingContext

首先,使用 context.getTokenType() 方法检查正在处理的 Token 是否是 Access Token。然后,使用 context.getClaims() 方法获取与正在构建的 JWT 相关的 Claim。最后,向 JWT 添加自定义 Claim。

在本例中,添加了两个 Claim(claim-1claim-2)和相应的值(value-1value-2)。

3.2、测试自定义 Claim {#32测试自定义-claim}

在测试中,使用 client_credentials Grant Type。

首先,将 AuthorizationServerConfig 中的 client_credentials Grant Type 定义为 RegisteredClient 对象中的 authorized Grant Type:

@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
      .clientId("articles-client")
      .clientSecret("{noop}secret")
      .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
      .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
      .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
      .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
      .redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
      .redirectUri("http://127.0.0.1:8080/authorized")
      .scope(OidcScopes.OPENID)
      .scope("articles.read")
      .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
}

然后,在 CustomClaimsConfigurationTest 类中创建一个测试用例:

@ActiveProfiles(value = "basic-claim")
public class CustomClaimsConfigurationTest {

    private static final String ISSUER_URL = "http://localhost:";
    private static final String USERNAME = "articles-client";
    private static final String PASSWORD = "secret";
    private static final String GRANT_TYPE = "client_credentials";

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int serverPort;

    @Test
    public void givenAccessToken_whenGetCustomClaim_thenSuccess() throws ParseException {
        String url = ISSUER_URL + serverPort + "/oauth2/token";
        HttpHeaders headers = new HttpHeaders();
        headers.setBasicAuth(USERNAME, PASSWORD);
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", GRANT_TYPE);
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
        ResponseEntity<TokenDTO> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, TokenDTO.class);

        SignedJWT signedJWT = SignedJWT.parse(response.getBody().getAccessToken());
        JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
        Map<String, Object> claims = claimsSet.getClaims();

        assertEquals("value-1", claims.get("claim-1"));
        assertEquals("value-2", claims.get("claim-2"));
    } 
    
    static class TokenDTO {
        @JsonProperty("access_token")
        private String accessToken;
        @JsonProperty("token_type")
        private String tokenType;
        @JsonProperty("expires_in")
        private String expiresIn;
        @JsonProperty("scope")
        private String scope;

        public String getAccessToken() {
            return accessToken;
        }
    }
}

上述的测试逻辑如下:

  • 首先为 OAuth2 令牌端点创建一个 URL。
  • 发送一个 POST 请求到 token 端点,并获取包含 TokenDTO 类的响应。这里,创建了一个 HTTP 请求实体,其中包含 Header(Basic Authentication)和参数(Grant Type)。
  • 使用 SignedJWT 类从响应中解析 Access Token。此外,还从 JWT 中提取 Claim 并将其存储在 Map<String, Object> 中。
  • 使用 JUnit 断言证明 JWT 中的指定 Claim 具有预期值。

该测试确认了 Token 编码流程工作正常,并生成了预期的 Claim。

还可以使用 curl 命令获取 Access Token :

curl --request POST \
  --url http://localhost:9000/oauth2/token \
  --header 'Authorization: Basic YXJ0aWNsZXMtY2xpZW50OnNlY3JldA==' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=client_credentials

如上,凭证编码为包含 client ID 和 client secret 的 Base64 字符串,以单个冒号 : 分隔。

现在,可以使用 Profile basic-claim 运行 Spring Boot 应用。

获得 Access Token 后,在 jwt.io 对其进行解码,可以在 Token Body 中找到测试 claim:

{
  "sub": "articles-client",
  "aud": "articles-client",
  "nbf": 1704517985,
  "scope": [
    "articles.read",
    "openid"
  ],
  "iss": "http://auth-server:9000",
  "exp": 1704518285,
  "claim-1": "value-1",
  "iat": 1704517985,
  "claim-2": "value-2"
}

可以看到,测试 Claim 的价值符合预期。

4、将 Authorities 作为自定义 Claim 添加到 JWT Access Token 中 {#4将-authorities-作为自定义-claim-添加到-jwt-access-token-中}

将 Authorities(权限)添加为自定义 Claim 到 JWT Access Token 中,通常是在 Spring Boot 应用中确保安全和管理访问的关键方面。权限通常由 Spring Security 中的 GrantedAuthority 对象表示,表示用户被允许执行的操作或角色。通过将这些权限作为自定义 Claim 包含在 JWT Access Token 中,为资源服务器提供了一种方便且标准化的方式来理解用户的权限。

4.1、将 Authorities 添加为自定义 Claim {#41将-authorities-添加为自定义-claim}

使用一个简单的内存用户配置,在 DefaultSecurityConfig 类中设置一组权限:

@Bean
UserDetailsService users() {
    UserDetails user = User.withDefaultPasswordEncoder()
      .username("admin")
      .password("password")
      .roles("USER")
      .build();
    return new InMemoryUserDetailsManager(user);
}

创建一个用户名为 admin、密码为 password、角色为 USER 的用户。

在 Access Token 中用这些 Authorities 填充自定义 Claim:

@Bean
@Profile("authority-claim")
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(@Qualifier("users") UserDetailsService userDetailsService) {
    return (context) -> {
      UserDetails userDetails = userDetailsService.loadUserByUsername(context.getPrincipal().getName());
      Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
      context.getClaims().claims(claims ->
         claims.put("authorities", authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList())));
    };
}

首先,定义一个实现 OAuth2TokenCustomizer<JwtEncodingContext> 接口的 lambda 函数。该函数会在编码过程中自定义 JWT。

然后,从注入的 UserDetailsService 中获取与当前 Principal(用户)相关的 UserDetails 对象。Principal 的 name 通常是用户名。

然后,检索与该用户相关联的 GrantedAuthority 对象集合。

最后,从 JwtEncodingContext 中检索 JWT Claim 并应用自定义。其中包括在 JWT 中添加名为 authorities 的自定义 Claim。此外,该 Claim 还包含从与用户关联的 GrantedAuthority 对象中获取的权限字符串列表。

4.2、测试 Authorities Claim {#42测试-authorities-claim}

配置了授权服务器后,进行测试。使用 GitHub 上的客户端-服务器项目

创建一个 REST API 客户端,从 Access Token 中获取 Claim 列表:

@GetMapping(value = "/claims")
public String getClaims(
  @RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
) throws ParseException {
    SignedJWT signedJWT = SignedJWT.parse(authorizedClient.getAccessToken().getTokenValue());
    JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
    Map<String, Object> claims = claimsSet.getClaims();
    return claims.get("authorities").toString();
}

@RegisteredOAuth2AuthorizedClient 注解用于 Spring Boot Controller 方法中,表示该方法希望 OAuth 2.0 授权客户端以指定的 client ID 注册。在本例中,client ID 是 articles-client-authorization-code

使用 authority-claim Profile 运行 Spring Boot 应用。

现在,打开浏览器并尝试访问 http://127.0.0.1:8080/claims 页面,会被自动重定向到 http://auth-server:9000/login URL 下的 OAuth 服务器登录页面。

提供正确的用户名和密码后,授权服务器会将我们重定向到请求的 URL,即 Claim 列表。

5、总结 {#5总结}

总之,向 JWT Access Token 添加自定义 Claim 的功能提供了一种强大的机制,可根据应用的特定需求定制 Token,并增强身份认证和授权系统的整体安全性和功能。

本文介绍了如何在 Spring Authorization Server 中为 JWT Access Token 添加自定义 Claim 和用户授权(Authorities)。


Ref:https://www.baeldung.com/spring-jwt-access-tokens-authorities-custom-claims

赞(2)
未经允许不得转载:工具盒子 » 在 Spring Authorization Server 中将 Authorities 作为自定义 Claim 添加到 JWT Access Token 中。