知識庫 / Spring / Spring Security RSS 訂閱

Spring Security – 從 JWT 中映射權限

Spring Security
HongKong
6
12:16 PM · Dec 06 ,2025

1. 簡介

本教程將演示如何自定義 JWT(JSON Web Token)聲明映射到 Spring Security 的 權限

2. 背景

當一個正確配置的基於 Spring Security 的應用程序接收到請求時,它會經歷一系列步驟,其本質目標是實現兩個目的:

  1. 驗證請求,使應用程序能夠知道是誰訪問它
  2. 確定是否允許經過驗證的請求執行相關的操作

對於使用 JWT 作為主要安全機制的應用程序,授權方面包括:

  1. 從 JWT 負載中提取聲明值,通常是 scopescp 聲明
  2. 將這些聲明映射到一組 GrantedAuthority 對象

一旦安全引擎設置好這些權限,它就可以評估當前請求是否存在任何訪問限制,並決定是否可以繼續執行。

3. 默認映射

默認情況下,Spring 使用一種直接策略將主張轉換為 GrantedAuthority 實例。首先,它提取 scopescp 主張,並將其拆分為字符串列表。然後,對於每個字符串,它使用前綴 SCOPE_ 加上 scope 值創建一個新的 SimpleGrantedAuthority

為了説明這種策略,讓我們創建一個簡單的端點,允許我們檢查應用程序可用的 Authentication 實例的一些關鍵屬性:

@RestController
@RequestMapping("/user")
public class UserRestController {
    
    @GetMapping("/authorities")
    public Map<String,Object> getPrincipalInfo(JwtAuthenticationToken principal) {
        
        Collection<String> authorities = principal.getAuthorities()
          .stream()
          .map(GrantedAuthority::getAuthority)
          .collect(Collectors.toList());
        
        Map<String,Object> info = new HashMap<>();
        info.put("name", principal.getName());
        info.put("authorities", authorities);
        info.put("tokenAttributes", principal.getTokenAttributes());

        if ( principal instanceof AccountToken ) {
          info.put( "account", ((AccountToken)principal).getAccount());
        }

        return info;
    }
}

在這裏,我們使用 JwtAuthenticationToken 參數,因為我們知道,在基於 JWT 的身份驗證中,它將是 Spring Security 創建的實際 Authentication 實現。我們通過提取其 name 屬性,創建結果,並從中獲取可用的 GrantedAuthority 實例以及 JWT 的原始屬性。

現在,假設我們調用該端點,並傳遞包含以下有效負載的編碼並簽名後的 JWT:

{
  "aud": "api://f84f66ca-591f-4504-960a-3abc21006b45",
  "iss": "https://sts.windows.net/2e9fde3a-38ec-44f9-8bcd-c184dc1e8033/",
  "iat": 1648512013,
  "nbf": 1648512013,
  "exp": 1648516868,
  "email": "[email protected]",
  "family_name": "Sevestre",
  "given_name": "Philippe",
  "name": "Philippe Sevestre",
  "scp": "profile.read",
  "sub": "eXWysuqIJmK1yDywH3gArS98PVO1SV67BLt-dvmQ-pM",
  ... more claims omitted
}
<p>響應應該看起來像一個包含三個屬性的 JSON 對象:</p>
{
  "tokenAttributes": {
     // ... token claims omitted
  },
  "name": "0047af40-473a-4dd3-bc46-07c3fe2b69a5",
  "authorities": [
    "SCOPE_profile",
    "SCOPE_email",
    "SCOPE_openid"
  ]
}

我們可以利用這些範圍來限制對我們應用程序特定部分的訪問,通過創建 SecurityFilterChain 實現:

@Bean
SecurityFilterChain customJwtSecurityChain(HttpSecurity http) throws Exception {
    // @formatter:off
    return http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwtConfigurer -> jwtConfigurer
                                .jwtAuthenticationConverter(customJwtAuthenticationConverter(accountService)))).build();
    // @formatter:on
}

請注意,我們故意避免使用了 WebSecurityConfigureAdapter。正如 所描述的,此類將在 Spring Security 版本 5.7 中被棄用,因此最好儘快遷移到新的方法。

或者,我們可以使用方法級別的註解和 SpEL 表達式來實現相同的結果:

@GetMapping("/authorities")
@PreAuthorize("hasAuthority('SCOPE_profile.read')")
public Map<String,Object> getPrincipalInfo(JwtAuthenticationToken principal) {
    // ... same code as before
}

最後,對於更復雜的場景,我們還可以直接訪問當前的 JwtAuthenticationToken,從而直接訪問所有 GrantedAuthorities

4. 自定義 SCOPE_ 前綴

作為改變 Spring Security 默認的聲明映射行為的第一個示例,讓我們看看如何將 SCOPE_ 前綴更改為其他值。 如文檔所述,此任務涉及兩個類:

  1. JwtAuthenticationConverter: 將原始 JWT 轉換為 AbstractAuthenticationToken
  2. JwtGrantedAuthoritiesConverter: 從原始 JWT 中提取 GrantedAuthority 實例集合。

在內部,JwtAuthenticationConverter 使用 JwtGrantedAuthoritiesConverterJwtAuthenticationToken 填充為 GrantedAuthority 對象以及其他屬性。

最簡單的方法是提供我們自己的 JwtAuthenticationConverter bean,並配置 JwtGrantedAuthoritiesConverter 為我們選擇的值。

@Configuration
@EnableConfigurationProperties(JwtMappingProperties.class)
@EnableMethodSecurity
public class SecurityConfig {
    // ... fields and constructor omitted
    @Bean
    public Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        if (StringUtils.hasText(mappingProps.getAuthoritiesPrefix())) {
            converter.setAuthorityPrefix(mappingProps.getAuthoritiesPrefix().trim());
        }
        return converter;
    }
    
    @Bean
    public JwtAuthenticationConverter customJwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter();
        return converter;
    }

這裏,JwtMappingProperties 只是一個我們將用於外部化映射屬性的 @ConfigurationProperties 類。雖然本示例中未展示,但我們將使用構造注入來將 mappingProps 字段初始化為從任何配置的 PropertySource 實例,從而提供足夠的靈活性,可以在部署時更改其值。

這個 @Configuration 類有兩個 @Bean 方法:jwtGrantedAuthoritiesConverter() 創建所需的 Converter,該 Converter 創建 GrantedAuthority 集合。在本例中,我們使用的是配置的前綴設置在配置屬性中的默認 JwtGrantedAuthoritiesConverter

接下來是 customJwtAuthenticationConverter(),其中我們構建了配置為使用自定義轉換器 JwtAuthenticationConverter。然後,Spring Security 將會將其作為其標準自動配置過程的一部分拾取,並替換默認的轉換器。

現在,一旦我們將 baeldung.jwt.mapping.authorities-prefix 屬性設置為某個值,例如 MY_SCOPE,並調用 /user/authorities, 我們將看到定製的權限:

{
  "tokenAttributes": {
    // ... token claims omitted 
  },
  "name": "0047af40-473a-4dd3-bc46-07c3fe2b69a5",
  "authorities": [
    "MY_SCOPE_profile",
    "MY_SCOPE_email",
    "MY_SCOPE_openid"
  ]
}

5. 使用自定義前綴在安全構造中

需要注意的是,通過更改權限服務器的前綴,將會影響任何依賴於這些前綴的授權規則。例如,如果我們更改前綴為 MY_PREFIX_,那麼任何基於 @PreAuthorize 的表達式,如果仍然假設使用默認前綴,將不再起作用。同樣,基於 HttpSecurity 的授權構造也適用。

解決此問題非常簡單。首先,讓我們在我們的 @Configuration 類中添加一個 @Bean 方法,該方法返回配置的前綴。由於此配置是可選的,因此我們必須確保在沒有提供任何前綴的情況下,返回默認值:

@Bean
public String jwtGrantedAuthoritiesPrefix() {
  return mappingProps.getAuthoritiesPrefix() != null ?
    mappingProps.getAuthoritiesPrefix() : 
      "SCOPE_";
}

現在,我們可以使用引用此 Bean 的 <em @bean-name 語法在 SpEL 表達式中使用。 這樣使用前綴 Bean 與 <em @PreAuthorize 的方式如下:

@GetMapping("/authorities")
@PreAuthorize("hasAuthority(@jwtGrantedAuthoritiesPrefix + 'profile.read')")
public Map<String,Object> getPrincipalInfo(JwtAuthenticationToken principal) {
    // ... method implementation omitted
}

我們也可以採用類似的方法來定義一個 SecurityFilterChain

@Bean
SecurityFilterChain customJwtSecurityChain(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests(auth -> {
        auth.requestMatchers("/user/**")
          .hasAuthority(mappingProps.getAuthoritiesPrefix() + "profile");
      })
      // ... other customizations omitted
      .build();
}

6. 自定義主人的姓名

有時,Spring 映射到 Authenticationname 屬性的標準聲明會提供一個對其實用性不高的值。 Keycloak 生成的 JWT 是一個很好的例子:

{
  // ... other claims omitted
  "sub": "0047af40-473a-4dd3-bc46-07c3fe2b69a5",
  "scope": "openid profile email",
  "email_verified": true,
  "name": "User Primo",
  "preferred_username": "user1",
  "given_name": "User",
  "family_name": "Primo"
}

在這種情況下,<em sub</em> 帶有內部標識符,但我們可以看到 <em preferred_username</em> 聲明具有更友好的值。<strong>我們可以通過將<em JwtAuthenticationConverter<em principalClaimName屬性設置為所需聲明名稱來輕鬆修改其行為</strong>

@Bean
public JwtAuthenticationConverter customJwtAuthenticationConverter() {

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());

    if (StringUtils.hasText(mappingProps.getPrincipalClaimName())) {
        converter.setPrincipalClaimName(mappingProps.getPrincipalClaimName());
    }
    return converter;
}

現在,如果我們將 baeldung.jwt.mapping.authorities-prefix 屬性設置為 “preferred_username”,那麼 /user/authorities 的結果將相應地發生變化:

{
  "tokenAttributes": {
    // ... token claims omitted 
  },
  "name": "user1",
  "authorities": [
    "MY_SCOPE_profile",
    "MY_SCOPE_email",
    "MY_SCOPE_openid"
  ]
}

7. 作用名稱映射

有時,我們需要將 JWT 中收到的作用名稱映射到內部名稱。例如,當同一個應用程序需要與由不同授權服務器生成的令牌進行交互,具體取決於其部署的環境時,就會出現這種情況。

我們可能會被誘惑擴展 JwtGrantedAuthoritiesConverter,但由於這是一個最終類,因此我們無法使用這種方法。相反,我們必須編寫自己的 Converter 類並將其注入到 JwtAuthorizationConverter 中。這個增強的映射器,MappingJwtGrantedAuthoritiesConverter,實現了 Converter&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt;&nbsp;並且看起來非常像原始的那個:

public class MappingJwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
    private static Collection<String> WELL_KNOWN_AUTHORITIES_CLAIM_NAMES = Arrays.asList("scope", "scp");
    private Map<String,String> scopes;
    private String authoritiesClaimName = null;
    private String authorityPrefix = "SCOPE_";
     
    // ... constructor and setters omitted

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        
        Collection<String> tokenScopes = parseScopesClaim(jwt);
        if (tokenScopes.isEmpty()) {
            return Collections.emptyList();
        }
        
        return tokenScopes.stream()
          .map(s -> scopes.getOrDefault(s, s))
          .map(s -> this.authorityPrefix + s)
          .map(SimpleGrantedAuthority::new)
          .collect(Collectors.toCollection(HashSet::new));
    }
    
    protected Collection<String> parseScopesClaim(Jwt jwt) {
       // ... parse logic omitted 
    }
}

在此,該類的關鍵在於映射步驟,我們使用提供的 scopes 映射圖將原始的 scopes 映射為映射後的 scopes。 任何沒有映射可用的傳入的 scopes 將會被保留。

最後,我們使用這個增強的轉換器在我們的 @Configuration 中的 jwtGrantedAuthoritiesConverter() 方法中使用它:

@Bean
public Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
    MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(mappingProps.getScopes());

    if (StringUtils.hasText(mappingProps.getAuthoritiesPrefix())) {
        converter.setAuthorityPrefix(mappingProps.getAuthoritiesPrefix());
    }
    if (StringUtils.hasText(mappingProps.getAuthoritiesClaimName())) {
        converter.setAuthoritiesClaimName(mappingProps.getAuthoritiesClaimName());
    }
    return converter;
}

8. 使用自定義的 JwtAuthenticationConverter

在當前場景中,我們將完全掌控 JwtAuthenticationToken 的生成過程。我們可以使用這種方法返回一個包含從數據庫中檢索的額外數據的擴展版本。

有兩條可能的替代方案可以替換標準的 JwtAuthenticationConverter。第一種,我們之前章節中使用的,是創建一個帶有 @Bean 方法,該方法返回我們的自定義轉換器。然而,這要求我們的自定義版本擴展 Spring 的 JwtAuthenticationConverter,以便自動配置過程可以將其選中。

第二種選擇是使用基於 HttpSecurity 的 DSL 方法,其中我們可以提供我們的自定義轉換器。我們將使用 oauth2ResourceServer 自定製器來實現,該自定製器允許我們插入一個實現更通用的接口 Converter<Jwt, AbstractAuthorizationToken> 的任何轉換器。

@Bean
SecurityFilterChain customJwtSecurityChain(HttpSecurity http) throws Exception {
    // @formatter:off
    return http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwtConfigurer -> jwtConfigurer
                                .jwtAuthenticationConverter(customJwtAuthenticationConverter(accountService)))).build();
    // @formatter:on
}

我們的 CustomJwtAuthenticationConverter 使用 AccountService (在線可用) 根據用户名聲明值檢索 Account 對象。然後它使用它來創建一個 CustomJwtAuthenticationToken,幷包含一個額外的訪問器方法來獲取賬户數據:

public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    // ...private fields and construtor omitted
    @Override
    public AbstractAuthenticationToken convert(Jwt source) {
        
        Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(source);
        String principalClaimValue = source.getClaimAsString(this.principalClaimName);
        Account acc = accountService.findAccountByPrincipal(principalClaimValue);
        return new AccountToken(source, authorities, principalClaimValue, acc);
    }
}

現在,讓我們修改我們的 /user/authorities 處理程序,使用我們增強的 Authentication

@GetMapping("/authorities")
public Map<String,Object> getPrincipalInfo(JwtAuthenticationToken principal) {

    // ... create result map as before (omitted)
    if (principal instanceof AccountToken) {
        info.put( "account", ((AccountToken)principal).getAccount());
    }
    return info;
}

採用這種方法的一個優勢是,我們現在可以輕鬆地在應用程序的其他部分使用我們的增強型身份驗證對象

例如,我們可以直接從內置變量authentication 中使用 SpEL 表達式訪問帳户信息:

@GetMapping("/account/{accountNumber}")
@PreAuthorize("authentication.account.accountNumber == #accountNumber")
public Account getAccountById(@PathVariable("accountNumber") String accountNumber, AccountToken authentication) {
    return authentication.getAccount();
}

此處,@PreAuthorize表達式強制執行路徑變量中傳入的accountNumber必須屬於用户。 這種方法尤其適用於與Spring Data JPA一起使用,如官方文檔中所述:官方文檔

9. 測試技巧

前面提供的示例假設我們已經擁有一個功能正常的身份提供者 (IdP),該 IdP 頒發基於 JWT 的訪問令牌。 使用我們之前已經涵蓋的嵌入式 Keycloak 服務器是一個不錯的選擇。 更多配置説明也可參見我們的使用 Keycloak 的快速指南。

請注意,這些説明涵蓋了如何註冊 OAuth 客户端。 對於實際測試,Postman 是一個不錯的工具,它支持授權碼流程。 關鍵細節在於如何正確配置 有效重定向 URI 參數。 由於 Postman 是一款桌面應用程序,它使用一個輔助站點位於 https://oauth.pstmn.io/v1/callback 以捕獲授權碼。 因此,在測試過程中,我們必須確保擁有互聯網連接。 如果無法做到,則可以使用不太安全的密碼授權流程。

無論選擇哪個 IdP 和客户端,我們都必須配置我們的資源服務器,使其能夠正確驗證收到的 JWT。 對於標準 OIDC 提供商,這意味着為 spring.security.oauth2.resourceserver.jwt.issuer-uri 屬性提供一個合適的數值。 Spring 將使用該文檔從那裏檢索所有配置詳情:.well-known/openid-configuration

在我們的情況下,Keycloak 領域的值 URI 是 http://localhost:8083/auth/realms/baeldung。 我們可以將瀏覽器指向以檢索完整文檔:http://localhost:8083/auth/realms/baeldung/.well-known/openid-configuration

10. 結論

在本文中,我們展示了多種自定義 Spring Security 從 JWT 聲明中映射權限的方法。

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

發佈 評論

Some HTML is okay.