1. 概述

在许多场景中,向JSON Web Token (JWT)访问令牌添加自定义声明至关重要。自定义声明允许我们在令牌载荷中包含额外信息。

本教程将学习如何在Spring Authorization Server中将资源所有者权限添加到JWT访问令牌中。

2. Spring Authorization Server

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

2.1 Maven依赖

首先在pom.xml中导入以下依赖:

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

或者添加更简洁的依赖:

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

2.2 项目设置

设置Spring Authorization Server用于签发访问令牌。为简化过程,我们将使用Spring Security OAuth Authorization Server应用程序。

假设使用GitHub上可用的授权服务器项目

3. 向JWT访问令牌添加基本自定义声明

在基于Spring Security OAuth2的应用程序中,可通过自定义授权服务器中的令牌创建流程向JWT访问令牌添加自定义声明。这类声明可用于向JWT注入额外信息,供资源服务器或认证授权流程中的其他组件使用。

3.1 添加基本自定义声明

使用OAuth2TokenCustomizer Bean添加自定义声明。授权服务器签发的每个访问令牌都将包含这些自定义声明。

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接口用于自定义OAuth 2.0令牌
  • 通过*context.getTokenType()*判断是否为访问令牌
  • 使用*context.getClaims()*获取JWT载荷并添加自定义声明
  • 示例添加了两个声明:claim-1claim-2

3.2 测试自定义声明

使用client_credentials授权类型进行测试。首先在RegisteredClient对象中定义授权类型:

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

创建测试用例:

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

测试流程:

  1. 构建OAuth2令牌接口URL
  2. 使用Basic认证和授权类型发起POST请求
  3. 解析返回的访问令牌
  4. 验证自定义声明值是否正确

测试确认令牌编码流程正常工作,声明按预期生成!

也可使用curl命令获取令牌:

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

使用basic-claim配置启动应用后,通过jwt.io解码令牌可见:

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

4. 向JWT访问令牌添加权限作为自定义声明

将权限作为自定义声明添加到JWT访问令牌,是Spring Boot应用安全管理的核心环节。Spring Security中的权限通常由GrantedAuthority对象表示,指示用户允许执行的操作或角色。通过将这些权限作为自定义声明包含在JWT中,可为资源服务器提供理解用户权限的标准化方式。

4.1 添加权限作为自定义声明

创建测试用户:

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

现在将用户权限填充到访问令牌的自定义声明中:

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

实现要点:

  1. 通过UserDetailsService获取当前用户的UserDetails
  2. 提取用户的GrantedAuthority集合
  3. 将权限列表添加到JWT的authorities声明中
  4. 使用Stream API将权限对象转换为字符串列表

4.2 测试权限声明

使用GitHub上的客户端-服务器项目进行测试。创建REST接口获取声明:

@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注解指示方法需要指定ID的OAuth 2.0授权客户端(此处为articles-client-authorization-code)。

使用authority-claim配置启动应用后:

  1. 访问http://127.0.0.1:8080/claims
  2. 自动重定向到http://auth-server:9000/login登录页
  3. 输入用户名admin和密码password
  4. 授权服务器重定向回原URL,显示权限列表

⚠️ 确保用户存在且配置正确,否则会触发认证失败

5. 结论

向JWT访问令牌添加自定义声明的能力,为定制令牌以满足应用特定需求提供了强大机制,同时增强了认证授权系统的安全性和功能性。

本文学习了在Spring Authorization Server中向JWT访问令牌添加自定义声明和用户权限的方法。

完整源代码请参考GitHub仓库


原始标题:Add Authorities as Custom Claims in JWT Access Tokens in Spring Authorization Server | Baeldung