<-- mermaid -->

Spring Security を使って独自の認可処理を実装した話

はじめに

皆さんこんにちは、株式会社ユーザベース SaaS事業の本谷です。今回はタイトルにある通り、Spring Security を使った独自の認可処理の実装方法について概要を紹介しようと思います。

Spring Security は認可に関する標準の実装が提供されており、一般的な認可処理を実現することは簡単です。例えばユーザごとに権限を持たせ、情報へのアクセスを許可または拒否することは多くのコードを書くことなく実現できます。一方、独自の実装をConfigクラスを使って設定することで、自由度の高い認可処理を実現することができ、より複雑な要件に対応できます。

今回、アプリケーションの要件は権限のチェックと同時に、ユーザに紐づく動的な情報も用いて認可を行うというものでした。このような複雑な認可の仕組みを実現するためには標準的な実装を利用することは困難であるため、独自の実装を行うことにしました。

前提

本記事内のコードは、説明のために一部分をわかりやすい内容に書き換えています。

各ライブラリや言語などのバージョンは下記のとおりです。

  • Java 17
  • Spring Boot 3.2.4

既存の認可処理

まず初めに、独自の認可処理を実装する前のコードを紹介します。

認可に関する設定は WebSecurityConfig クラスにまとめられています。以下はそのクラス内の認可に関連する設定を行うsecurityFilterChainメソッドの一部抜粋です。

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/page/download").hasAuthority("DOWNLOAD") // 1
            .requestMatchers("/manage/**").hasAuthority("ADMIN") // 2
          .anyRequest().authenticated() // 3
      );
      
      return http;
}

Spring Security ではこのようにメソッドチェーンでパスに対する認可処理を設定します。基本的には requestMatchers で対象のパスを、hasAuthority で必要な権限を指定します。

上記のコードでは次のような認可の設定をしています。

  1. /page/download に対するHTTPリクエストは DOWNLOAD 権限が付与されているユーザのみに制限
  2. /manage から始まるすべてのHTTPリクエストは ADMIN 権限が付与されているユーザのみに制限
  3. それ以外のHTTPリクエストは認証されたユーザのみに制限

hasAuthorityの仕組み

hasAuthority メソッドは、引数で与えられた文字列とユーザの権限情報を比較し、HTTPリクエストを許可するかどうかを判断します。一般的にユーザの権限情報は SimpleGrantedAuthority クラスで文字列として保持されるため、Spring Security は文字列同士の比較にて認可の判断を決定します。

例えば上記の例だと、 /page/download に対してアクセスできるユーザは以下のような SimpleGrantedAuthority が関連づいている必要があります。

new User("uzabase.taro", "strong-password", List.of(new SimpleGrantedAuthority("DOWNLOAD")));

このようにユーザに紐付いた権限を元にした認可は標準の実装を利用することで簡単に実現することができます。

独自の認可処理を実装する

新しく発生した要件は、権限のチェックと同時に、ユーザに紐づく動的な情報も用いて認可を行うというものでした。例えば、ダウンロード権限を持っていて、かつダウンロード回数が上限を超えていないかどうかをチェックする、といった内容です。この要件を満たすためには hasAuthority メソッドのみで実装することは困難です。なぜなら、hasAuthority はユーザ権限のみがチェック対象となるため、ダウンロード回数のような動的に変わる値を元に認可の判断をすることが難しいからです。

Spring Security は特殊な要件にも対応できるような手段が用意されており、今回は access メソッドを利用して要件を満たす実装を行いました。access メソッドは AuthorizationManager を実装したオブジェクトを受け取り、認可処理を自由度高く実装できます。AuthorizationManager は関数型インターフェイスなのでラムダ式を使用することができます。具体的な実装を下記に示します。

http.authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/download").access((Supplier<Authentication> authentication, RequestAuthorizationContext object) -> {
            // 1
            Authentication auth = authentication.get();
            
            // 2
            if (!auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList().contains("DOWNLOAD")) {
                return new AuthorizationDecision(false);
            }
            
            // 3
            if (!checkDownloadCountLimitUsecase.execute(auth)) {
                return new AuthorizationDecision(false);
            }
            
            // 4
            return new AuthorizationDecision(true);
        }));

上記のコードでは /download へのHTTPリクエストに対して、以下の認可処理を行っています。

  1. 認証済みのユーザ情報を取得する
  2. ダウンロード権限を持っていなければ、アクセスを拒否する
  3. ダウンロード回数が制限を上回っていれば、アクセスを拒否する
  4. ダウンロード権限を持っていて、かつダウンロード回数が制限数以下であれば、アクセスを許可する

このようにユーザに紐づく特定の属性だけではなく、ダウンロード回数といった様々な情報を元に認可を判断することができます。上記の例では触れていませんが、 access の2つ目の引数 RequestAuthorizationContext から HttpServletRequest を取得できるので、リクエストパスなどの情報を用いることもできます。

独自の認可処理を実装する上で注意すべきこと

独自の認可処理を実装する場合には、セキュリティのリスクに注意が必要です。 access メソッドを利用した認可処理は自由度が高く、幅広い要件に対応できますが、複雑になればなるほど意図せず脆弱性が入り込む可能性が高まります。独自に認可処理を実装する場合には網羅的なテストを用意することを事前に検討しておくと良いでしょう。

例えば、本記事で紹介した例は access の引数にラムダ式を渡していますが、AuthorizationManager を実装したクラスを用意することで、独自の認可処理の部分だけを取り出してテストすることが容易になります。そうすることで考えられる入力値に対する網羅的な単体テストが実施しやすくなるでしょう。

おわりに

今回は Spring Security を使った独自の認可処理の実装方法を紹介しました。 Spring Security はよくある一般的なケースに対する標準的な実装を提供しつつ、特殊な要件にも対応できるよう選択肢も与えられています。今回の記事では認可に関して書きましたが、認証についても同様に様々な選択肢を取ることができます。みなさんも認証、認可において Spring Security を使ってみてはどうでしょうか?

Page top