今回やること
keycloakでuser,manager,adminというロールと各ロールを持つユーザを作成し、ロールに応じた認可をSpringBootのAPIサーバで実現します
※keycloakの認証認可の仕組みなどの詳細は割愛します
事前設定
keycloakのロール設定
keycloakで作成したユーザ
ユーザ | ロール |
---|---|
user | user |
manager | manager |
admin_user | admin |
さっそくやってみよう
1.build.gradleに依存関係を追加
まずはSpringBootに認証と認可を組み込むためにSpringSecurityの導入と、SpringBootアプリケーションをリソースサーバとして用いるために必要な設定を行います
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への設定
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/demoRealm
issuer-uri
Jwtトークンのiss
クレームに含まれる値のことで、この値をもとに設定をおこない、認可サーバの公開鍵の取得や、その後のJwt検証を行います。
(検証時には 発行者だけでなく有効期限の検証も自動的におこなってくれます)
サーバ起動時の自動構成
上記の設定をしたことで、サーバ起動時に以下の内容が自動構成されます。
-
issuer-uri
に指定されたURLの.well-known/openid-configuration
エンドポイントにアクセスします
※token_endpointなどのメタデータと呼ばれる各種URLやサポートする機能の一覧をJSONで返却するエンドポイント -
そのJSONから
jwks_uri
を取得します
※jwks
= JSON Web Key Set
公開鍵を含むキーの集合をJSON形式で表現したもの
この公開鍵は、JWTの署名を検証するために使用されます -
jwks_uri
から公開鍵を取得しキャッシュします -
リクエストヘッダの
Authorization:Bearer <token>
を検出し、取得した公開鍵でJWTの署名を検証します
3.SpringSecurityでの設定
@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
という権限に変換する処理(④に対応)があります。
※公式ドキュメントの画像を拝借
SpringSecurityでは特に設定しなくてもデフォルトで③④を自動設定してくれますが、今回のkeycloakのJwtにおいてはJwtAuthenticationConverter
の設定が不足していたため、カスタマイズして設定しました。
通常の挙動としては、Jwtのscope
属性から権限を抽出しますが、今回のkeycloakのJwtの形は以下のようになっているため、そこからroles
を抽出していきます。
"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を定義する
ここで定義したものをsecurityFilterChain
のoauth2ResourceServer
に渡してコンバータを設定します。
.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での設定
@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サーバのようなリクエストを受けてリソースを提供するサーバ
認可サーバとは
アクセストークンを発行し管理するサーバ