1. 序章

このチュートリアルでは、 JWT (JSON Web Token)クレームからSpringSecurityのAuthoritiesへのマッピングをカスタマイズする方法を示します。

2. バックグラウンド

適切に構成されたSpringSecurityベースのアプリケーションが要求を受信すると、基本的に2つの目標を目指す一連の手順を実行します。

  • リクエストを認証して、アプリケーションが誰がリクエストにアクセスしているかを認識できるようにします
  • 認証されたリクエストが関連するアクションを実行できるかどうかを決定します

JWTをメインのセキュリティメカニズムとして使用するアプリケーションの場合、承認の側面は次の要素で構成されます。

  • JWTペイロードからクレーム値を抽出します。通常はscopeまたはscpクレームです。
  • これらのクレームを一連のGrantedAuthorityオブジェクトにマッピングする

セキュリティエンジンがこれらの権限を設定すると、現在のリクエストにアクセス制限が適用されるかどうかを評価し、続行できるかどうかを判断できます

3. デフォルトのマッピング

すぐに使用できるSpringは、単純な戦略を使用して、クレームをGrantedAuthorityインスタンスに変換します。 まず、scopeまたはscpクレームを抽出し、文字列のリストに分割します。 次に、文字列ごとに、プレフィックス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());
        
        return info;
    }
}

ここでは、 JwtAuthenticationToken 引数を使用します。これは、JWTベースの認証を使用する場合、これがSpringSecurityによって作成された実際の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
}

応答は、次の3つのプロパティを持つJSONオブジェクトのようになります。

{
  "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 {
    return http.authorizeRequests(auth -> {
      auth.antMatchers("/user/**")
        .hasAuthority("SCOPE_profile");
    })
    .build();
}

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_プレフィックスを別のものに変更する方法を見てみましょう。 ドキュメントで説明されているように、このタスクには2つのクラスが関係しています。

  • JwtAuthenticationConverter :生のJWTをAbstractAuthenticationTokenに変換します
  • JwtGrantedAuthoritiesConverter :生のJWTからGrantedAuthorityインスタンスのコレクションを抽出します。

内部的には、 JwtAuthenticationConverter は、 JwtGrantedAuthoritiesConverter を使用して、JwtAuthenticationTokenGrantedAuthorityオブジェクトと他の属性を設定します。

このプレフィックスを変更する最も簡単な方法は、独自のJwtAuthenticationConverter Bean を提供し、JwtGrantedAuthoritiesConverterを独自の選択肢の1つに構成することです。

@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クラスです。 このスニペットには示されていませんが、コンストラクターインジェクションを使用して、構成済みの PropertySource から入力されたインスタンスで、 mappingProps フィールドを初期化します。これにより、デプロイ時に値を変更するのに十分な柔軟性が得られます。時間。

この@Configurationクラスには2つの@Beanメソッドがあります。 ]コレクション。 この場合、構成プロパティで設定されたプレフィックスで構成されたストック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を参照して使用できます。 @ SpEL式の構文 。 これは、@PreAuthorizeでプレフィックスBeanを使用する方法です。

@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.authorizeRequests(auth -> {
        auth.antMatchers("/user/**")
          .hasAuthority(mappingProps.getAuthoritiesPrefix() + "profile");
      })
      // ... other customizations omitted
      .build();
}

6. プリンシパルの名前のカスタマイズ

標準のsubは、SpringがAuthentication’nameプロパティにマップする値があまり役に立たないと主張することがあります。 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"
}

この場合、 sub には内部識別子が付いていますが、Preferred_usernameクレームの方がわかりやすい値であることがわかります。 希望するクレーム名でprincipalClaimNameプロパティを設定することにより、JwtAuthenticationConverterの動作を簡単に変更できます。

@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 、実装コンバータ >> 元のものとよく似ています:

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

ここで、このクラスの重要な側面はマッピングステップです。ここでは、提供されたスコープマップを使用して、元のスコープをマップされたスコープに変換します。 また、マッピングが利用できない着信スコープは保持されます。

最後に、この拡張コンバーターを @ConfigurationjwtGrantedAuthoritiesConverter()メソッドで使用します。

@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を置き換える方法は2つあります。 前のセクションで使用した最初の方法は、カスタムコンバーターを返す@Beanメソッドを作成することです。 ただし、これは、自動構成プロセスが選択できるように、カスタマイズされたバージョンがSpringのJwtAuthenticationConverterを拡張する必要があることを意味します。

2番目のオプションは、 HttpSecurity ベースのDSLアプローチを使用することです。このアプローチでは、カスタムコンバーターを提供できます。 これは、 oauth2ResourceServer カスタマイザー。これにより、はるかに一般的なインターフェイスを実装するコンバーターをプラグインできます。 コンバータ

@Bean
SecurityFilterChain customJwtSecurityChain(HttpSecurity http) throws Exception {
    return http.oauth2ResourceServer(oauth2 -> {
        oauth2.jwt()
          .jwtAuthenticationConverter(customJwtAuthenticationConverter());
      })
      .build();
}

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ハンドラーを変更しましょう。

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

このアプローチを採用する利点の1つは、アプリケーションの他の部分で拡張認証オブジェクトを簡単に使用できることです。 たとえば、組み込み変数authenticationからSpEL式のアカウント情報に直接アクセスできます。

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

ここで、 @PreAuthorize 式は、パス変数で渡されたaccountNumberがユーザーに属することを強制します。 このアプローチは、公式ドキュメントで説明されているように、SpringDataJPAと組み合わせて使用する場合に特に役立ちます。

9. テストのヒント

これまでの例では、JWTベースのアクセストークンを発行するIDプロバイダー(IdP)が機能していることを前提としています。 良いオプションは、すでにここでカバーしている組み込みの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. 結論

この記事では、SpringSecurityがJWTの主張から権限をマップする方法をカスタマイズするさまざまな方法を示しました。 いつものように、完全なコードはGitHubから入手できます。