在 Spring Authorization Server 中將權威作為自定義聲明添加到 JWT 訪問令牌中

Spring Security
Remote
1
09:39 AM · Nov 30 ,2025

1. 概述

將自定義聲明添加到 JSON Web Token (JWT) 訪問令牌可能在許多場景中都至關重要。自定義聲明允許我們在令牌有效負載中包含額外的信息。

在本教程中,我們將學習如何在 Spring 授權服務器中將資源所有者權威性添加到 JWT 訪問令牌。

2. Spring Authorization Server

Spring Authorization Server 是 Spring 生態系統中一個新項目,旨在為 Spring 應用程序提供 Authorization Server 支持。 它旨在簡化使用熟悉的、靈活的 Spring 編程模型實現 OAuth 2.0 和 OpenID Connect (OIDC) 授權服務器的過程。

2.1. Maven 依賴項

首先導入 spring-boot-starter-webspring-boot-starter-securityspring-boot-starter-testspring-security-oauth2-authorization-server 依賴項到 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.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>

或者,我們可以將 spring-boot-starter-oauth2-authorization-server 依賴項添加到我們的 pom.xml 文件中:

<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. Add Basic Custom Claims to JWT Access Tokens

In a Spring Security OAuth2-based application, we can add custom claims to JWT access tokens by customizing the token creation process in the Authorization Server. This type of claim can be useful for injecting additional information into JWTs, which can then be used by resource servers or other components in the authentication and authorization flow.

3.1. Add Basic Custom Claims

We can add our custom claims to an access token using the OAuth2TokenCustomizer<JWTEncodingContext> bean. By using it, every access token that is issued by the authorization server will have the custom claims populated.

Let’s add the OAuth2TokenCustomizer bean in the DefaultSecurityConfig class:

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

The OAuth2TokenCustomizer interface is part of the Spring Security OAuth2 library and is used to customize OAuth 2.0 tokens. In this case, it specifically customizes JWT tokens during the encoding process.

The lambda expression passed to the jwtTokenCustomizer() bean defines the customization logic. The context parameter represents the JwtEncodingContext during the token encoding process.

First, we use the context.getTokenType() method to check whether the token being processed is an access token. Then, we obtain the claims associated with the JWT being constructed by using the context.getClaims() method. Finally, we add custom claims to the JWT.

In this example, two claims (“claim-1” and “claim-2“) with corresponding values (“value-1” and “value-2“) are added.

3.2. Test the Custom Claims

For testing, we are going to use the client_credentials grant type.

First, we’ll define theclient_credentials grant type from AuthorizationServerConfig as an authorized grant type in the RegisteredClient object:

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

Then, let’s create a test case in the CustomClaimsConfigurationTest class:

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

Let’s walk through the key parts of our test to understand what is going on:

  • Start by constructing a URL for the OAuth2 token endpoint.
  • Retrieve a response containing the TokenDTO class from a POST request to the token endpoint. Here, we create an HTTP request entity with headers (basic authentication) and parameters (grant type).
  • Parse the Access Token from the response using the SignedJWT class. Also, we extract claims from the JWT and store them in the Map<String, Object>.
  • Assert that specific claims in the JWT have expected values using JUnit assertions.

This test confirms that our token encoding process is working properly and our claims are being generated as expected. Awesome!

In addition, we can get the access token using the curl command:

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

Here, the credentials are encoded as a Base64 string of the client ID and client secret, delimited by a single colon “:”.

Now, we can run our Spring Boot application with the profile basic-claim.

If we obtain an access token and decode it using jwt.io, we find the test claims in the token’s body:

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

As we can see, the value of the test claims is as expected.

We’ll discuss adding authority as claims to access tokens in the following section.

4. 添加自定義聲明作為 JWT 訪問令牌的權限

將權限作為自定義聲明添加到 JWT 訪問令牌中通常是 Securing 和管理 Spring Boot 應用程序訪問的關鍵方面。 權限,通常在 Spring Security 中表示為 GrantedAuthority 對象,指示用户允許執行的操作或角色。 通過將這些權限作為自定義聲明包含在 JWT 訪問令牌中,我們提供了一種方便且標準化的方式,用於資源服務器理解用户的權限。

4.1. 將權限作為自定義聲明

首先,我們使用一個簡單的內存用户配置,其中包含一套權限,在 DefaultSecurityConfig 類中:

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

創建一個用户名“admin”密碼“password”和角色“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())));
    };
}

首先,我們定義一個實現了 OAuth2TokenCustomizer<JwtEncodingContext>接口的 lambda 函數。 此函數自定義 JWT 在編碼過程中。

然後,我們從注入的 UserDetailsService 中檢索 UserDetails 對象,該對象的名稱通常是用户名。

之後,我們檢索與用户關聯的 GrantedAuthority 對象集合。

最後,我們從 JwtEncodingContext 中檢索 JWT 聲明,並應用自定義設置。 這包括將名為“authorities”的自定義聲明添加到 JWT 中。 此外,此聲明包含來自與用户關聯的 GrantedAuthority 對象字符串列表。

4.2. 測試權限聲明

現在我們已配置授權服務器,讓我們對其進行測試。 為此,我們將使用來自同一 GitHub 文件夾中的客户端-服務器項目。

讓我們創建一個 REST API 客户端,用於檢索訪問令牌中的聲明列表:

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

在 Spring Boot 控制器方法中使用了 @RegisteredOAuth2AuthorizedClient 註解,以指示方法預期註冊與指定客户端 ID 的 OAuth 2.0 授權客户端。 在這種情況下,客户端 ID 是“articles-client-authorization-code“。

讓我們運行我們的 Spring Boot 應用程序,具有 authority-claim 配置文件。

現在,當我們去瀏覽器並嘗試訪問 http://127.0.0.1:8080/claims頁面時,我們將自動重定向到 auth-server:9000/login URL 下的 OAuth 服務器登錄頁面。

在提供適當的用户名和密碼後,授權服務器會將我們重定向回請求的 URL,該 URL 包含聲明列表。

5. 結論

總而言之,將自定義聲明添加到 JWT 訪問令牌中提供了一種強大的機制,可以根據應用程序的特定需求定製令牌,並增強身份驗證和授權系統的整體安全性和功能。

在本文中,我們學習瞭如何在 Spring 授權服務器中添加自定義聲明和用户權限到 JWT 訪問令牌。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.