知識庫 / Spring / Spring Security RSS 訂閱

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

Spring Security
HongKong
4
11:20 AM · Dec 06 ,2025

1. 概述

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

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

2. Spring 授權服務器

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

2.1. Maven 依賴

首先,導入以下依賴項到 <pom.xml> 中:spring-boot-starter-web, spring-boot-starter-security, spring-boot-starter-test, 和 spring-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>

當然,以下是翻譯後的內容:

或者,我們可以將 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 授權服務器。為了簡化操作,我們將使用 Spring Security OAuth 授權服務器應用程序。

假設我們正在使用授權服務器項目可在 GitHub 上找到

3. 為 JWT 訪問令牌添加基本自定義聲明

在基於 Spring Security OAuth2 的應用程序中, 我們可以通過自定義授權服務器中 JWT 創建過程來為 JWT 訪問令牌添加自定義聲明。 這種類型的聲明可以用於將額外信息注入到 JWT 中,然後由資源服務器或其他身份驗證和授權流程中的組件使用。

3.1. 添加基本自定義聲明

我們可以使用 OAuth2TokenCustomizer<JWTEncodingContext> Bean 將我們的自定義聲明添加到訪問令牌中。通過使用該 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 接口是 Spring Security OAuth2 庫的一部分,用於自定義 OAuth 2.0 令牌。在此,它特別是在編碼過程中自定義 JWT 令牌。

將傳遞給 jwtTokenCustomizer() bean 的 lambda 表達式定義了自定義邏輯。context 參數在令牌編碼過程中代表 JwtEncodingContext

首先,我們使用 context.getTokenType() 方法來檢查正在處理的令牌是否為訪問令牌。然後,我們通過使用 context.getClaims() 方法獲取與正在構建的 JWT 關聯的聲明。最後,我們向 JWT 中添加自定義聲明。

在此示例中,兩個聲明 (“claim-1” 和 “claim-2“) 及其對應的值 (“value-1” 和 “value-2“) 被添加。

3.2. 測試自定義聲明

為了進行測試,我們將使用 client_credentials 授權模式。

首先,我們將從 AuthorizationServerConfig 中定義 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);
}

然後,讓我們在 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 請求中檢索響應,該響應包含 TokenDTO 類。在此,我們創建一個帶有標頭(基本身份驗證)和參數(grant type)的 HTTP 請求實體。
  • 使用 SignedJWT 類解析響應中的訪問令牌。此外,我們從 JWT 中提取聲明並將其存儲在 Map<String, Object> 中。
  • 使用 JUnit 斷言驗證 JWT 中的特定聲明具有預期值。

此測試確認我們的令牌編碼過程正在正常工作,並且我們的聲明正在按預期生成。太棒了!

此外,可以使用 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

此處,憑據以客户端 ID 和客户端密鑰的 Base64 字符串編碼,並用單個冒號“:”分隔。

現在,我們可以使用 basic-claim 配置文件運行我們的 Spring Boot 應用程序。

如果我們獲取一個訪問令牌並使用 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. 添加自定義聲明作為權威

首先,我們使用一個簡單的內存用户配置,其中包含在 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 中檢索與當前 principal(用户)關聯的 UserDetails 對象。 principal 的名稱通常是用户名。

接下來,我們檢索與用户關聯的 GrantedAuthority 對象集合。

最後,我們從 JwtEncodingContext 中檢索 JWT 聲明,並應用自定義。 這包括向 JWT 添加一個名為“authorities”的自定義聲明。 此外,該聲明包含來自與用户關聯的 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();
}

@RegisteredOAuth2AuthorizedClient 註解用於在 Spring Boot 控制器方法中指示該方法期望註冊一個帶有指定客户端 ID 的 OAuth 2.0 授權客户端。 在這種情況下,客户端 ID 是 “articles-client-authorization-code”。

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

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

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

5. 結論

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

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

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

發佈 評論

Some HTML is okay.