6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

keycloakを認可サーバ、SpringBootをリソースサーバとしてロールベースの認可を実現する

Posted at

今回やること

keycloakでuser,manager,adminというロールと各ロールを持つユーザを作成し、ロールに応じた認可をSpringBootのAPIサーバで実現します

※keycloakの認証認可の仕組みなどの詳細は割愛します

事前設定

keycloakのロール設定

image.png

keycloakで作成したユーザ

image.png

ユーザ ロール
user user
manager manager
admin_user admin

さっそくやってみよう

1.build.gradleに依存関係を追加

まずはSpringBootに認証と認可を組み込むためにSpringSecurityの導入と、SpringBootアプリケーションをリソースサーバとして用いるために必要な設定を行います

build.gradle
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-security")
	implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
}

spring-boot-starter-security

認証と認可の機能を提供するライブラリ

spring-boot-starter-oauth2-resource-server: OAuth 2.0

リソースサーバー機能を提供するライブラリで、Jwt検証を簡素化 してくれます。
⇒ Jwtでエンコードされたベアラートークンをサポートするような、リソースサーバに必要な依存関係が内包されています。
⇒ この依存関係を使用することで、リソースサーバ 起動時に自動的にJwtでエンコードされたベアラートークンを検証するようにサーバを構成 してくれます。

2.application.ymlへの設定

application.yml
spring:
  security:
   oauth2:
     resourceserver:
       jwt:
         issuer-uri: http://localhost:8080/realms/demoRealm

issuer-uri

Jwtトークンのissクレームに含まれる値のことで、この値をもとに設定をおこない、認可サーバの公開鍵の取得や、その後のJwt検証を行います。
(検証時には 発行者だけでなく有効期限の検証も自動的におこなってくれます)

サーバ起動時の自動構成

上記の設定をしたことで、サーバ起動時に以下の内容が自動構成されます。

  1. issuer-uriに指定されたURLの.well-known/openid-configurationエンドポイントにアクセスします
    ※token_endpointなどのメタデータと呼ばれる各種URLやサポートする機能の一覧をJSONで返却するエンドポイント

  2. そのJSONからjwks_uriを取得します
    jwks = JSON Web Key Set
    公開鍵を含むキーの集合をJSON形式で表現したもの
    この公開鍵は、JWTの署名を検証するために使用されます

  3. jwks_uriから公開鍵を取得しキャッシュします

  4. リクエストヘッダのAuthorization:Bearer <token>を検出し、取得した公開鍵でJWTの署名を検証します

3.SpringSecurityでの設定

SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
        .cors(withDefaults())
        .csrf(csrf->csrf.disable())
            .authorizeHttpRequests(authorize -> authorize
                // 認証が必要なエンドポイント
                .requestMatchers("/api/user").hasRole("user") // userロールのユーザのみ許可
                .requestMatchers("/api/admin").hasRole("admin") // adminロールのユーザのみ許可
                .requestMatchers("/api/manager").permitAll() // 誰でも許可
                // 認証が不要なエンドポイント
                .anyRequest().permitAll()
            ).oauth2ResourceServer(oauth2->oauth2.jwt(jwt->jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));

        return http.build();
    }

    // コンバータの設定
    @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter(){
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            if (realmAccess == null || !realmAccess.containsKey("roles")) {
                return List.of();
            }
            List<String> roles = (List<String>) realmAccess.get("roles");
            return roles.stream()
                .map(role ->new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
        });

        return converter;
    }
}

コンバータの設定

そもそもJwt認証の際に裏でおこなわれていることとして、Jwtのデコード処理(③に対応)とJwtをgrantedAuthorityという権限に変換する処理(④に対応)があります。
※公式ドキュメントの画像を拝借
image.png

SpringSecurityでは特に設定しなくてもデフォルトで③④を自動設定してくれますが、今回のkeycloakのJwtにおいてはJwtAuthenticationConverterの設定が不足していたため、カスタマイズして設定しました。

通常の挙動としては、Jwtのscope属性から権限を抽出しますが、今回のkeycloakのJwtの形は以下のようになっているため、そこからrolesを抽出していきます。

Jwt抜粋
"realm_access": {
    "roles": [
      "offline_access",
      "default-roles-demorealm",
      "uma_authorization",
      "user"
    ]
  }

具体的には以下の設定を行いました。

コンバータ設定
    @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter(){
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            if (realmAccess == null || !realmAccess.containsKey("roles")) {
                return List.of();
            }
            List<String> roles = (List<String>) realmAccess.get("roles");
            return roles.stream()
                .map(role ->new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
        });

        return converter;
    }

JwtAuthenticationConverterを定義する

ここで定義したものをsecurityFilterChainoauth2ResourceServerに渡してコンバータを設定します。

.oauth2ResourceServer(oauth2->oauth2.jwt(jwt->jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));

JwtAuthenticationConverterにgrantedAuthoritiesConverterを設定する

grantedAuthoritiesConverterとは、JwtをGrantedAuthroritiesのコレクションに変換するためのものです。
今回はJwtの realm_access -> roles にある権限を抽出してgrantedAuthorityとしてセットするようにしました。
(参考)
https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.html

GrantedAuthorityに変換する際、プレフィックスにROLEを付す

後述しますが、プレフィックスをつけることで認可判断の際のメソッドのショートカットを利用することが出来るようになるので、付与しておくとよいでしょう。
デフォルトのコンバータでは、変換のタイミングでSCOPEというプレフィックスが付きます。

認可判断の設定

Jwtから権限を抽出したら、実際にそれを用いてAPIの認可判断をする必要があります。
設定としては2パターンあります。

①SecurityFilterChainでの設定

SecurityConfig.java
 @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
        .cors(withDefaults())
        .csrf(csrf->csrf.disable())
            .authorizeHttpRequests(authorize -> authorize
                // 認証が必要なエンドポイント
                .requestMatchers("/api/user").hasRole("user") // userロールのユーザのみ許可
                .requestMatchers("/api/admin").hasRole("admin") // adminロールのユーザのみ許可
                .requestMatchers("/api/manager").permitAll() // 誰でも許可
                // 認証が不要なエンドポイント
                .anyRequest().permitAll()
            ).oauth2ResourceServer(oauth2->oauth2.jwt(jwt->jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));

        return http.build();
    }

具体的には、requestMachers("URL").hasRole("ロール名")requestMachers("URL").permitAll()で認可判断をしています。
よく使われるものとしては以下の3つのように思います。

permitAll()
認可を必要としない、パブリックなエンドポイントであることを要求しています。

hasAuthority("権限名")
引数に指定した権限名が、GrantedAuthorityに含まれていることを要求しています。

hasRole("ロール名")
hasAuthorityのショートカットで、プレフィックスにROLEの付いた権限がGrantedAuthorityに含まれていることを要求します。(先ほどGrantedAuthority変換時にプレフィックスをつけたのはこのためです)

②各URIごとにControllerで設定(メソッドレベルでの設定)

前提としてメソッドレベルでの認可をアクティブにするには任意の @Configuration クラスに @EnableMethodSecurityのアノテーションを付ける必要がありますのでご留意ください。

@RestController
public class SampleController {

    // managerのみがアクセス
    @PreAuthorize("hasRole('manager')")
    @GetMapping("/api/manager")
    public ResponseEntity<SampleResponse> manager() {
        return ResponseEntity.ok(new SampleResponse("成功"));
    }

}

先ほど/api/managerのURLにはpermitAllを設定しておりパブリックなエンドポイントとなっていましたが、Controller部分で認可判断を設定してみようと思います。

具体的には以下のアノテーションで設定できます。

 @PreAuthorize("hasRole('manager')")

ここでは、指定された式であるhasRole('manager') がパスした場合にのみメソッドを呼び出すことができることを意味しています。

ここまで設定が出来れば、APIリクエストにベアラートークンを含めることで、適切に認可判断ができるようになります。お疲れさまでした。

おわり

やはり確認すべきは公式ドキュメント様ですね。
プロジェクトでは依存関係だったり設定ファイルは既に設定されていたので、自分で一通り作成ができてよかったです。
最近は、公式ドキュメントだけでは例が少ないようなライブラリなども存在するので、公式以外から良い情報、良いコードを手に入れる手段を模索中です。
githubとかなんですかね。。。アドバイスいただけると幸いです。

メモ

リソースサーバとは

APIサーバのようなリクエストを受けてリソースを提供するサーバ

認可サーバとは

アクセストークンを発行し管理するサーバ

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?