これまで2回に分けて、API でログイン及び認証チェックを行う方法を説明しました。
この記事は認可です。調べば調べるほどできることがたくさんあり、一度での把握は難しいため、この記事では認可の基本的な流れを紹介します。
- 認証の基本的な流れの理解とログインAPIの実装
- 認証後に JWT を生成/Authrization ヘッダに付与して、次回以降の API の認証チェックを実施
網羅的な説明は他の記事でたくさんあるので別記事に譲ります。この記事は網羅的な説明ではなくその前段で、そもそもの動きを理解した人に向け手助けになればと思います。
間違いあれば指摘いただけると嬉しいです。
前提
過去の続きです。過去のコードについては上記リンクを参照ください。バージョンは以下です。
- Java 17
- Spring 2.7.10
Spring Boot 2.7.10 に含まれる Spring Security Starter は 2023/3/25 時点で v2 系では Spring Security 5.7.7 です。5.8 から追加された機能もあるので、そのあたりは Spring Boot のバージョンを 3 にして別の機会に確認予定です。
今回の範囲においては 5.7.7 でも 5.8 以降でも同じ書き方になる(はずです)。
認証情報 / 認可のための情報
認可の処理の前に、認証情報(ロール情報なども含む)の参照方法です。
今回はもともと springframework が持っている User(org.springframework.security.core.userdetails.User
)を継承した MyUserDetails
を作りましたが、この User が持つプロパティ authorities
に認可のための権限情報(GrantedAuthority
を実装したクラスのインスタンスのコレクション: Collection<? extends GrantedAuthority>
)が含まれています。
前回の記事では SimpleGrantedAuthority
を利用しています。これは標準で用意されている GrantedAuthority
の具体クラスの1つで、シンプルに識別文字列(ロール)を持つクラスです。他には LDAP 関連の情報ももつ LdapAuthority
などがあります。
公式に書かれている通り、認証が成功した場合 認証情報は SecurityContextHolder に格納され、その後 SecurityContextPersistenceFilter
のフィルタ処理によって SecurityContext
を HttpSession
に保存します。これにより、以下のコードでどこからでも値が取れるようになります。
SecurityContext.Holder.getContext().getAuthentication()
認可の方法
ここから具体的に認可の方法です。Spring Security は、メソッド呼び出しやAPIリクエスト実行時のインターセプターが提供されています。
リソース(URLベース)
URLベースのアクセス制御です(/admin/** は admin権限の場合のみと言ったもの)。
以前(Spring 徹底入門の書籍)に書かれているものは AccessDecisionManagerとAccessDecisionVoterでしたが、今は AuthorizationManager を利用するようです。
そして、この AuthorizationManager は AuthorizationFilter
で呼び出されます。
AuthorizationFilter
はどこで呼ばれるかというと、こちらも 公式 にありますが SecurityFilterChain を追加するときに、 authorizeRequests
を使用する代わりに authorizeHttpRequests
を使用すると、AuthorizationFilter
が呼ばれるとのことです(下に例を書いてます)。
AuthorizationFilter
が呼ばれると、SecurityContextHolder
から認証情報を取得し、RequestMatcherDelegatingAuthorizationManager
が実行されます。この中で authorizaHttpRequests
で設定した内容に従ってチェックを行い、アクセス制御結果(OK / NG) を返します。
以下は、前回までの記事の http.quthorizaHttpRequests を2行変えています。/api/hello
は hoge ロールを必要とし、それ以外は拒否します。
http.authorizeHttpRequests(authz -> authz
.antMatchers("/").permitAll()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/hello").hasRole("hoge")
.anyRequest().denyAll()
);
RequestMatcherDelegatingAuthorizationManager
の実行では具体的には check
というメソッドが呼ばれます。以下のスクリーンショットを見てわかる通り、mappings の値が上で定義したものになっていることがわかります。これをぐるぐる回して、今回アクセスされたリソースが mappings にマッチするかをチェックをしています。
mappings にマッチするものがあった場合、さらに以下の check
メソッド(クラスは同じ)がよばれます。ここではパターンが一致したものに対して authority のチェックを行います。
今回は hasRole(“hoge”) を指定したので、ROLE_hoge が authority にあればアクセスOKになります。が、今回のユーザは持っていないためNGになるはずです。
実際に 403 になりました。
HTTP/1.1 403
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sat, 25 Mar 2023 06:00:21 GMT
Connection: close
一応、hasRole
(“ADMIN”) にするとOKでした。というのも確認済み。
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 3
Date: Sat, 25 Mar 2023 06:39:08 GMT
Connection: close
foo
今回は hasRole
だけですが、他にも色々なメソッドが用意されています。
主な種類 | 意味 |
---|---|
hasRole | 指定したロールを持っているか(ROLE_XXXX の XXXX 部分を指定する) こちら によると、ROLE_ から始まってない場合は ROLE_ が補完される |
hasAnyRole | 複数のロールが指定でき、いずれかを持っていればOK |
hasAuthority | 指定した Authority を持っている場合 (ROLE_XXXX の場合、ROLE_XXXX を書かないといけない |
hasAnyAuthority | 複数の Autorityが指定でき、いずれかを持っていればOK |
permitAll | 常にOK |
denyAll | 常にNG |
autenticated | 認証されていればOK |
access | カスタム処理を定義(booleanを返却)することができる ※ |
※ access は、5.7 の公式ドキュメントを見ると、String を引数にとりカスタム処理を含む式ベースの指定ができるようですが、実際に試そうとすると String の引数は許可されておらず試せませんでした。5.8以降であれば WebExpressionAuthorizationManager が提供されており、これを利用する旨の記述になっていました。
v6系でも同様なので、WebExpressionAuthorizationManager を使ったやり方が正しそうですが、5.7 ではこのクラスも提供されていないのでご注意を。
access を利用すると簡単にカスタム処理が使え、機能としては便利と思うので別途検証できればと思います。
URLベースでロールあるいはカスタム処理でアクセス制御が簡単にできますね。
メソッド
メソッドレベルでも認可チェックができます。@EnableMethodSecurity
を指定することで機能を有効にでき、以下が用意されているようです。利用時はアノテーションをつけるだけでいけます。
アノテーション | 説明 |
---|---|
@PreAuthorize | メソッド実行前にチェック。OKならメソッドが実行される。 実際の処理は PreAuthorizeAuthorizationManager が実行される模様。 |
@PostAuthorize | メソッド実行後にチェック。OKならメソッド実行結果が返却される。 実際の処理は PostAuthorizeAuthorizationManager が実行される模様。 |
@PreFilter | メソッド実行前にチェック。 引数となるオブジェクトのリストのうち、指定した条件に合致するもののみが処理の対象となる。 実際の処理は PreFilterAuthorizationMethodInterceptor が実行される模様。 |
@PostFilter | メソッド実行後にチェック。 戻り値となるオブジェクトのリストのうち、指定した条件に合致するもののみが返却の対象となる。 実際の処理は PostFilterAuthorizationMethodInterceptor が実行される模様。 |
PreFilter と PostFilter はこちら に具体的な説明もあります。
以下はコード例です。
動作確認結果は 403 になったかならないかだけの違いなのでここでは載せていません。
コメントに書いている通り、引数や戻り値、ユーザ情報といったものを利用してチェックを行うことができます。
@Slf4j
@RestController
public class MyRestController {
// PreAuthorize
// ADMIN2 というロールはもってないのでメソッドは実行されず、403 になる
// hasRole を ADMINなどユーザが持っているロールと一致する場合はメソッドが実行される
// この例では言及していないが、引数の情報を式に入れることもできる。「#引数名」とすることで
// API 実行時のパラメータにアクセスし、Pre判定を行うことができる。
// PostAutorize
// 「returnObject」は戻りのオブジェクトを指す。ここでは "foo"を返しているので、
// 「returnObject」は foo。 foo であれば結果が返る。
// もし条件式を hoge などにして条件が false になる場合は同じく 403 が帰る。
// いずれも principal を利用すると、ユーザ情報にアクセスもできる
// 使えるものは以下参照
// https://spring.pleiades.io/spring-security/reference/5.7.0/servlet/authorization/expression-based.html
//
// このうち、hasPermission について、詳細は以下
// https://spring.pleiades.io/spring-security/reference/servlet/authorization/expression-based.html#el-permission-evaluator
@PreAuthorize("hasRole('ADMIN2')")
@PostAuthorize("returnObject == 'foo'")
@GetMapping("/api/hello")
public String hello() {
log.info("hello");
return "foo";
}
なお、このアノテーションの中は SpEL (Spring Expression Language)という記法で書くことができ、and や or といったものもかけます。各種型の呼び出し方含め以下にまとまっています。
終わりに
今回は未確認だけど、追加で調べたいこと
- access を用いたカスタム処理(リソースベースのカスタム認可チェック)
- hasPermission のカスタム処理(メソッドベースのカスタム認可チェック)
- 認可エラー時のレスポンスのハンドリング
- ドメインオブジェクトのACL
- ロールの階層化