9.3. 認可


9.3.1. Overview

本節では、Spring Securityが提供している認可機能について説明する。

認可処理は、アプリケーションの利用者がアクセスできるリソースを制御するための処理である。
利用者がアクセスできるリソースを制御するためのもっとも標準的な方法は、リソース(又はリソースの集合)毎にアクセスポリシーを定義しておき、利用者がリソースにアクセスしようとした時にアクセスポリシーを調べて制御する方法である。
アクセスポリシーには、どのリソースにどのユーザーからのアクセスを許可するかを定義する。
Spring Securityでは、以下の3つのリソースに対してアクセスポリシーを定義することができる。
  • Webリソース

  • Javaメソッド

  • ドメインオブジェクト [1]

  • 画面項目

本節では、「Webリソース」「Javaメソッド」「画面項目」のアクセスに対して認可処理を適用するための実装例(定義例)を紹介しながら、Spring Securityの認可機能について説明する。


9.3.1.1. 認可処理のアーキテクチャ

Spring Securityは、以下のような流れで認可処理を行う。

../_images/AuthorizationArchitecture.png

認可処理のアーキテクチャ

項番

説明

(1)
クライアントは、任意のリソースにアクセスする。
(2)
AuthorizationFilterクラスは、AuthorizationManagerインタフェースのメソッドを呼び出し、リソースへのアクセス権の有無をチェックする。
(3)
AuthorizationManagerの実装クラスであるRequestMatcherDelegatingAuthorizationManagerが、受け取ったリクエストを適切なAuthorizationManagerに振り分けてアクセス権の有無をチェックする。
(4)
AuthorizationFilterは、AuthorizationManagerによってアクセス権が付与された場合に限り、リソースへアクセスする。

9.3.1.1.1. ExceptionTranslationFilter

ExceptionTranslationFilterは、認可処理(AuthorizationManager)で発生した例外をハンドリングし、クライアントへ適切なレスポンスを行うためのSecurity Filterである。
デフォルトの実装では、未認証ユーザーからのアクセスの場合は認証を促すレスポンス、認証済みのユーザーからのアクセスの場合は認可エラーを通知するレスポンスを返却する。

9.3.1.1.2. AuthorizationFilter

AuthorizationFilterは、HTTPリクエストに対して認可処理を適用するためのSecurity Filterで、実際の認可処理はAuthorizationManagerに委譲する。
AuthorizationManagerインタフェースのメソッドを呼び出す際には、クライアントがアクセスしようとしたリソースに指定されているアクセスポリシーを連携する。
アクセスが許可されると、AuthorizationFilterFilterChainを続行する。

9.3.1.1.3. AuthorizationManager

AuthorizationManagerは、アクセスしようとしたリソースに対してアクセス権があるかチェックを行うためのインタフェースである。
アクセス権がないと判断した場合は、AccessDeniedExceptionを発生させアクセスを拒否する。
Spring Securityでは以下の実装クラスを提供している。

Spring Securityが提供するAuthorizationManagerの実装クラス

クラス名

説明

RequestMAtcherDelegatingAuthorizationManager
リクエストに一致するRequestMatcherを基に、認可処理を特定のAuthorizationManagerに移譲する。
AuthorityAuthorizationManager
Spring Securityが提供する一般的なAuthorizationManager
認証情報(Authentication)に指定された権限が含まれているかどうかを評価し、現在のユーザーが認可されているかどうかを判別する。
AuthenticatedAuthorizationManager
匿名ユーザー、完全認証ユーザー、リメンバー認証ユーザーを区別するために使用される。
JSR250AuthorizationManager
認証情報(Authentication)がJSR-250セキュリティアノテーションから指定された権限を含んでいるかどうかを評価する。
SecuredAuthorizationManager
認証情報(Authentication)がSpring SecurityのSecuredアノテーションから指定された権限を含んでいるかどうかを評価する。
PreAuthorizeAuthorizationManager
認証情報(Authentication)がPreAuthorizeアノテーションから指定された権限を含んでいるかどうかを評価する。
PreAuthorizaAuthorizationMAnager
認証情報(Authentication)がPostAuthorizeアノテーションから指定された権限を含んでいるかどうかを評価する。
Spring Securityが提供するAuthorizationManager以外に、独自に構築したRequestMatcherDelegatingAuthorizationManagerを使用することも可能である。
詳しくは、Configure RequestMatcherDelegatingAuthorizationManagerを参照されたい。

9.3.2. How to use

認可機能を使用するために必要となるbean定義例(アクセスポリシーの指定方法)や実装方法について説明する。

9.3.2.1. アクセスポリシーの記述方法

アクセスポリシーの記述方法を説明する。

Spring Securityは、アクセスポリシーを指定する記述方法としてHttpSecurityに対するメソッドチェーンをサポートしている。
また、<intercept-url>JSP Taglibなど、式が必要な場合に備えSpring Expression Language(SpEL)をサポートしている。

Tip

SpELの使い方については本節でも紹介するが、より詳しい使い方を知りたい場合はSpring Framework Documentation -Spring Expression Language (SpEL)-を参照されたい。


9.3.2.1.1. 代表的な認可メソッド

HttpSecurity#authorizeHttpRequests(Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer)のCustomizerを実装し、URLパターン経由でHttpServletRequestに基づいてアクセスを制限することができる。
代表的な認可メソッドは以下の通り。
Spring Securityが提供している認可メソッド

メソッド名

説明

hasRole(String role)
ログインユーザーが、引数に指定したロールを保持している場合にtrueを返却する。
ロールのROLE_ プレフィックスは省略可能である。
hasAnyRole(String... roles)
ログインユーザーが、引数に指定したロールのいずれかを保持している場合にtrueを返却する。
ロールのROLE_ プレフィックスは省略可能である。
anonymous()
ログインしていない匿名ユーザーの場合にtrueを返却する。
rememberMe()
Remember Me認証によってログインしたユーザーの場合にtrueを返却する。
authenticated()
ログイン中の場合にtrueを返却する。
fullyAuthenticated()
Remember Me認証ではなく通常の認証プロセスによってログインしたユーザーの場合にtrueを返却する。
permitAll()
常にtrueを返却する。
denyAll()
常にfalseを返却する。(デフォルト値)
access(AuthorizationManager<RequestAuthorizationContext> manager)
カスタムAuthorizationManagerを使用して認可処理を実施する。

Note

Spring Securityの認可処理のデフォルト値はdenyAllであるため、業務要件に応じ適切に認可する範囲を指定する必要がある。


9.3.2.1.2. Built-InのCommon Expressions

Spring Securityが用意している共通的なExpressionは以下の通り。

Spring Securityが提供している共通的なExpression

Expression

説明

hasRole(String role)
ログインユーザーが、引数に指定したロールを保持している場合にtrueを返却する。
ロールのROLE_ プレフィックスは省略可能である。
hasAnyRole(String... roles)
ログインユーザーが、引数に指定したロールのいずれかを保持している場合にtrueを返却する。
ロールのROLE_ プレフィックスは省略可能である。
isAnonymous()
ログインしていない匿名ユーザーの場合にtrueを返却する。
isRememberMe()
Remember Me認証によってログインしたユーザーの場合にtrueを返却する。
isAuthenticated()
ログイン中の場合にtrueを返却する。
isFullyAuthenticated()
Remember Me認証ではなく通常の認証プロセスによってログインしたユーザーの場合にtrueを返却する。
permitAll
常にtrueを返却する。
denyAll
常にfalseを返却する。(デフォルト値)
principal
認証されたユーザーのユーザー情報(UserDetailsインタフェースを実装したクラスのオブジェクト)を返却する。
authentication
認証されたユーザーの認証情報(Authenticationインタフェースを実装したクラスのオブジェクト)を返却する。

Note

Expressionを使用した認証情報へのアクセス

Expressionとしてprincipalauthenticationを使用すると、ログインユーザーのユーザー情報や認証情報を参照することができるため、ロール以外の属性を使ってアクセスポリシーを設定することが可能になる。

Note

Spring Secuirtyが提供するその他のExpression

上記に記載した以外にも、Spring Securityではログインユーザーが保持する権限を確認するExpressionとして、hasAuthority(String authority)hasAnyAuthority(String... authorities)hasPermission(Object target, Object permission)hasPermission(Object targetId, String targetType, Object permission)を提供している。

ユーザの属性により権限をグループ化したものがロールであり、一般的には個々の権限による認可ではなくロールによる認可が推奨される。

Spring Securityの認可においてはいずれもログインユーザが「指定した権限(ロール)を保持しているか」を確認するため利用方法に違いはないが、権限名はロール名と異なりROLE_のようなプレフィックスがないため、権限の定義と認可で名称を完全一致させる必要がある。

Note

Spring Securityの認可処理のデフォルト値はdenyAllであるため、業務要件に応じ適切に認可する範囲を指定する必要がある。


9.3.2.1.3. Built-InのWeb Expressions

Spring Securityが用意しているWebアプリケーション向けExpressionは以下の通り。

Spring Securityが提供するWebアプリケーション向けExpression

Expression

説明

hasIpAddress(String ipAddress)
リクエスト元のIPアドレスが、引数に指定したIPアドレス体系に一致する場合にtrueを返却する。

9.3.2.1.4. 演算子の使用

演算子を使用した判定も行うことができる。
以下の例では、ロールと、リクエストされたIPアドレス両方に合致した場合、アクセス可能となる。
  • SpringSecurityConfig.javaの定義例

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        // omitted
        http.authorizeHttpRequests(authz -> authz
                // omitted
                .access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasIpAddress('192.168.10.1')"))
                // omitted
                );
        return http.build();
    }
    

使用可能な演算子一覧

演算子

説明

[式1] and [式2]
式1、式2が、どちらも真の場合に、真を返す。
[式1] or [式2]
いずれかの式が、真の場合に、真を返す。
![式]
式が真の場合は偽を、偽の場合は真を返す。

9.3.2.2. Webリソースへの認可

Spring Securityは、サーブレットフィルタの仕組みを利用してWebリソース(HTTPリクエスト)に対して認可処理を行う。


9.3.2.2.1. 認可処理の適用

Webリソースに対して認可処理を適用する場合は、以下のようなbean定義を行う。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    // omitted
    http.authorizeHttpRequests(authz -> authz
            .requestMatchers(new AntPathRequestMatcher("/**")).authenticated() // (1)
            );
    // omitted
    return http.build();
}

項番

説明

(1)
HttpSecurityのメソッドチェーンにより、HTTPリクエストに対してアクセスポリシーを定義する。
ここでは、authenticated()メソッドを呼び出し「Webアプリケーション配下の全てのリクエストに対して認証済みのユーザーのみアクセスを許可する」というアクセスポリシーを定義している。

9.3.2.2.2. アクセスポリシーの定義

bean定義ファイルを使用して、Webリソースに対してアクセスポリシーを定義する方法について説明する。


9.3.2.2.2.1. アクセスポリシーを適用するWebリソースの指定

HttpSecurity#authorizeHttpRequests(Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer)のCustomizerを実装し、アクセスポリシーを設定する。

  • SpringSecurityConfig.javaの定義例

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        // omitted
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers(new AntPathRequestMatcher("/admin/accounts/**", HttpMethod.GET.name())).hasRole("ACCOUNT_MANAGER") // (1)(4)
                .requestMatchers(new AntPathRequestMatcher("/admin/configurations/**"))
                        .access(new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1') and hasRole('CONFIGURATION_MANAGER')")) // (2)
                .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasAnyRole("USER", "ADMIN")
                .requestMatchers(new AntPathRequestMatcher("/**")).denyAll()
                );
        http.requiresChannel(channel -> channel.anyRequest().requiresSecure()); // (3)
        // omitted
        return http.build();
    }
    
    メソッドチェーンによるアクセスポリシーを適用する設定

    項番

    説明

    (1)
    パスパターンに一致するリソースを適用対象とするため、AuthorizationManagerRequestMatcherRegistry.requestMatchersRequestMatcherオブジェクトを設定する。
    上記設定例ではAntPathRequestMatcherを指定している。設定できる項目は以下となる。
    AntPathRequestMatcherの設定項目

    変数名

    説明

    pattern
    Ant形式又は正規表現で指定したパスパターンに一致するリソースを適用対象にする。
    httpMethod
    指定したHTTPメソッド(GET,POSTなど)を使ってアクセスがあった場合に適用対象にする。
    (2)
    Expressionsを使用する場合はAuthorizedUrl#accessを使用し、WebExpressionAuthorizationManagerを設定する。
    WebExpressionAuthorizationManagerのコンストラクタ引数に認可制御用のExpressionsを設定する。
    (3)
    HttpSecurity#requiresChannel(Customizer<ChannelSecurityConfigurer<HttpSecurity>.ChannelRequestMatcherRegistry> requiresChannelCustomizer)のCustomizerを実装し、指定したプロトコルを強制することができる。
    上記設定例ではhttpでリクエストされた際にhttpsへリダイレクトする処理となる。
    (4)
    requestMatchersが返却する
Spring Securityは定義した順番でリクエストとのマッチング処理を行い、最初にマッチした定義を適用する。
そのため、bean定義ファイルを使用してアクセスポリシーを指定する場合も定義順番には注意が必要である。

Warning

Spring Security 4.1以降、Spring Securityがデフォルトで使用しているAntPathRequestMatcher のパスマッチングの仕様が大文字・小文字を区別する様になった。

例えば以下に示すように、/Todo/Listというパスが割り当てられたSpring MVCのエンドポイントに対してアクセスポリシーを定義する場合は、パスパターンについて/Todo/List/Todo/*のように大文字・小文字をそろえる必要がある。

誤って/todo/list/todo/**など大文字・小文字がそろっていない値を指定してしまうと、意図した認可制御が行われなくなるので注意されたい。

  • Spring MVCのエンドポイントの実装例

    @RequestMapping(value="/Todo/List")
    public String viewTodoList(){
        // omitted
    }
    
  • アクセスポリシーの定義例

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        // omitted
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers(new AntPathRequestMatcher("/Todo/List")).authenticated()
                // omitted
                );
        // omitted
        return http.build();
    }
    

Note

Spring MVCとSpring Securityでは、リクエストとのマッチングの仕組みが厳密には異なっており、この差異を利用してSpring Securityの認可機能を突破し、ハンドラメソッドにアクセスできる脆弱性が存在する。

本事象の詳細は「CVE-2016-5007 Spring Security / MVC Path Matching Inconsistency」を参照されたい。

trimTokensプロパティにtrueを設定したorg.springframework.util.AntPathMatcherのBeanがSpring MVCに適用されている場合に、本事象が発生する。

デフォルト値はfalseであるため、意図的に変更しない限り本事象は発生しない。


9.3.2.2.2.2. アクセスポリシーの設定例
ログインユーザーに「ROLE_USER」「ROLE_ADMIN」というロールがある場合を例に、設定例を示す。
  • SpringSecurityConfig.java

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        // omitted
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers(new AntPathRequestMatcher("/reserve/**")).hasAnyRole("USER", "ADMIN") // (1)
                .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN") // (2)
                .requestMatchers(new AntPathRequestMatcher("/**")).denyAll() // (3)
                );
        // omitted
        return http.build();
    }
    

    項番

    説明

    (1)
    /reserve/**」にアクセスするためには、「ROLE_USER」もしくは「ROLE_ADMIN」ロールが必要である。
    ログインユーザーが指定したロールを保持していれば真を返す。
    (2)
    /admin/**」にアクセスするためには、「ROLE_ADMIN」ロールが必要である。
    ログインユーザーが指定したロールを保持していれば真を返す。
    (3)
    denyAllを全てのパターンに設定し、
    権限設定が記述されていないURLに対してはどのユーザーもアクセス出来ない設定としている。

Note

URLパターンの記述順序について

クライアントからのリクエストに対して、intercept-urlで記述されているパターンに、上から順にマッチさせ、マッチしたパターンに対してアクセス認可を行う。

そのため、パターンの記述は、必ず、より限定されたパターンから記述すること。


9.3.2.2.2.3. パス変数の参照

Spring Security 4.1以降では、アクセスポリシーを適用するリソースを指定する際にパス変数[2]を使用することができ、アクセスポリシーの定義内で#パス変数名と指定することで参照できる。

ただし、拡張子を付けてアクセス可能なパスに対してパス変数を使用するアクセスポリシーを定義する場合は、パス変数値に拡張子部分が格納されない様に定義する必要がある。

例えば、パターンに/users/{userName}と定義し、/users/personName.jsonというリクエストパスを送信した際、アクセスポリシーの定義内で参照しているパス変数#userNameにはpersonNameではなくpersonName.jsonが格納され、意図しない認可制御が行われてしまう。

この事象を防ぐためには、「拡張子を付けたパスに対するアクセスポリシー」を定義した後に、「拡張子を付けないパスに対するアクセスポリシー」を定義する必要がある。

以下の例は、ログインユーザが自身のユーザ情報のみアクセスできる様にアクセスポリシーを定義している。

  • ワイルドカードを使用する場合

    • SpringSecurityConfig.javaの定義例

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        // omitted
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers(new AntPathRequestMatcher("/users/{userName}.*"))
                        .access(new WebExpressionAuthorizationManager("isAuthenticated() and #userName == principal.username")) // (1)
                .requestMatchers(new AntPathRequestMatcher("/users/{userName}/**"))
                        .access(new WebExpressionAuthorizationManager("isAuthenticated() and #userName == principal.username")) // (2)
                // omitted
                );
        // omitted
        return http.build();
    }
    

    項番

    説明

    (1)
    「拡張子を付けたパスに対するアクセスポリシー」を定義する。
    (2)
    「拡張子を付けないパスに対するアクセスポリシー」を定義する。
    ワイルドカードを使用して/users/{userName}で始まるパスに対するアクセスポリシーを定義する。

  • ワイルドカードを使用しない場合

    • SpringSecurityConfig.javaの定義例

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        // omitted
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers(new AntPathRequestMatcher("/users/{userName}.*"))
                        .access(new WebExpressionAuthorizationManager("isAuthenticated() and #userName == principal.username")) // (1)
                .requestMatchers(new AntPathRequestMatcher("/users/{userName}/"))
                        .access(new WebExpressionAuthorizationManager("isAuthenticated() and #userName == principal.username")) // (2)
                .requestMatchers(new AntPathRequestMatcher("/users/{userName}"))
                        .access(new WebExpressionAuthorizationManager("isAuthenticated() and #userName == principal.username")) // (2)
                // omitted
                );
        // omitted
        return http.build();
    }
    

    項番

    説明

    (1)
    「拡張子を付けたパスに対するアクセスポリシー」を定義する。
    (2)
    「拡張子を付けないパスに対するアクセスポリシー」を定義する。
    ワイルドカードを使用しない場合、Spring MVCとSpring Securityのパスマッチングの差を吸収するために
    末尾が”/“で終わるパスに対するアクセスポリシーも定義する。

9.3.2.3. メソッドへの認可

Spring Securityは、Spring AOPの仕組みを利用してDIコンテナで管理しているBeanのメソッド呼び出しに対して認可処理を行う。

メソッドに対する認可処理は、ドメイン層(サービス層)のメソッド呼び出しに対して行うことを想定して提供されている。
メソッドに対する認可処理を使用すると、ドメインオブジェクトのプロパティを参照することができるため、きめの細かいアクセスポリシーの定義を行うことが可能になる。

9.3.2.3.1. AOPの有効化

メソッドへの認可処理を使用する場合は、メソッド呼び出しに対して認可処理を行うためのコンポーネント(AOP)を有効化する必要がある。
AOPを有効化すると、アクセスポリシーをメソッドのアノテーションに定義できるようになる。

Spring Securityは、以下のアノテーションをサポートしている。

  • @PreAuthorize@PostAuthorize@PreFilter@PostFilter

  • JSR-250 (jakarta.annotation.securityパッケージ)のアノテーション(@RolesAllowedなど)

  • @Secured

本ガイドラインでは、アクセスポリシーをExpressionで使用することができる@PreAuthorize@PostAuthorizeを使用する方法を説明する。

  • SpringSecurityConfig.javaの定義例

@Configuration
@EnableMethodSecurity // (1)
public class SpringSecurityConfig {

項番

説明

(1)
@EnableMethodSecurityアノテーションをコンフィグレーションクラスに付与すると、メソッド呼び出しに対する認可処理を行うAOPが有効になる。
なお、prePostEnabledプロパティはデフォルトでtrueとなっており、Expressionを指定してアクセスポリシーを定義できるアノテーションが有効となっている。

9.3.2.3.2. 認可処理の適用

メソッドに対して認可処理を適用する際は、アクセスポリシーを指定するアノテーションを使用して、メソッド毎にアクセスポリシーを定義する。


9.3.2.3.3. アクセスポリシーの定義

9.3.2.3.3.1. メソッド実行前に適用するアクセスポリシーの指定

メソッドの実行前に適用するアクセスポリシーを指定する場合は、@PreAuthorizeを使用する。

@PreAuthorizevalue属性に指定したExpressionの結果がtrueになるとメソッドの実行が許可される。
下記例では、管理者以外は、他人のアカウント情報にアクセスできないように定義している。
  • @PreAuthorizeの定義例

// (1) (2)
@PreAuthorize("hasRole('ADMIN') or (#username == principal.username)")
public Account findOne(String username) {
    return accountRepository.findByUsername(username);
}

項番

説明

(1)
認可処理を適用したいメソッドに、@PreAuthorizeを付与する。
(2)
value属性に、メソッドに対してアクセスポリシーを定義する。
ここでは、「管理者の場合は全てのアカウントへのアクセスを許可する」「管理者以外の場合は自身のアカウントへのアクセスのみ許可する」というアクセスポリシーを定義している。
ここでポイントになるのは、Expressionの中からメソッドの引数にアクセスしている部分である。
具体的には、「#username」の部分が引数にアクセスしている部分である。
Expression内で「# + 引数名」形式のExpressionを指定することで、メソッドの引数にアクセスすることができる。

Tip

引数名を指定するアノテーション

Spring Securityは、クラスに出力されているデバッグ情報から引数名を解決する仕組みになっているが、アノテーション(@org.springframework.security.core.parameters.P)を使用して明示的に引数名を指定することもできる。

以下のケースにあてはまる場合は、アノテーションを使用して明示的に変数名を指定する。

  • クラスに変数のデバッグ情報を出力しない

  • Expressionの中から実際の変数名とは別の名前を使ってアクセスしたい (例えば短縮した名前)

    @PreAuthorize("hasRole('ADMIN') or (#username == principal.username)")
    public Account findOne(@P("username") String username) {
        return accountRepository.findByUsername(username);
    }
    

なお、#usernameと、メソッドの引数である usernameの名称が一致している場合は @Pを省略することが可能である。

ただし、Spring Securityは引数名の解決を、実装クラスの引数名を使用して行っているため@PreAuthorizeアノテーションをインターフェースに定義している場合には、実装クラスの引数名を、 @PreAuthorize 内で指定した #username と一致させる必要があるので、注意されたい。

JDK 8 から追加されたコンパイルオプション(-parameters)を使用すると、メソッドパラメータにリフレクション用のメタデータが生成されるため、アノテーションを指定しなくても引数名が解決される。

Warning

Spring 5から、SpringのコアAPIにnull-safetyの機能が取り入れられており、SpELが解釈される際のnullに対する動作も変更(SPR-15540)されている。

例えば@PreAuthorizeの引数(#xxx)や、@PostAuthorizeの戻り値(resultObject)がMapを含む場合、Mapから値を取得するSpELでキー値にnullとなる値を入力すると、Spring 4以前ではそのままMapnullが渡され該当する値がないためnullが返却されていたが、Spring 5以降ではキーとなるSpELを評価した結果に対するnullチェックが追加されており、nullの場合はIllegalStateExceptionが発生する。

そのため、キーとする値に対して事前にnullチェックを行うなど、nullを考慮した実装が必要となる。


9.3.2.3.3.2. メソッド実行後に適用するアクセスポリシーの指定

メソッドの実行後に適用するアクセスポリシーを指定する場合は、@PostAuthorizeを使用する。

@PostAuthorizevalue属性に指定したExpressionの結果がtrueになるとメソッドの実行結果が呼び出し元に返却される。
下記例では、所属する部署が違うユーザーのアカウント情報にアクセスできないように定義している。
  • @PostAuthorizeの定義例

@PreAuthorize("...")
@PostAuthorize("(returnObject == null) " +
        "or (returnObject.departmentCode == principal.account.departmentCode)")
public Account findOne(String username) {
    return accountRepository.findByUsername(username);
}
ここでポイントになるのは、Expressionの中からメソッドの返り値にアクセスしている部分である。
具体的には、「returnObject.departmentCode」の部分が返り値にアクセスしている部分である。
Expression内で「returnObject」を指定すると、メソッドの返り値にアクセスすることができる。

9.3.2.4. 画面項目への認可

Spring Securityは、JSPタグライブラリを使用してJSPの画面項目に対して認可処理を適用することができる。
また、Spring Security Dialectは、Spring Securityが提供するJSPタグライブラリと同等の認可処理をThymeleafに適用することができる。

ここでは最もシンプルな定義を例に、画面項目のアクセスに対して認可処理を適用する方法について説明する。


9.3.2.4.1. アクセスポリシーの定義

JSPタグライブラリを使用してJSPの画面項目に対してアクセスポリシーを定義する際は、表示を許可する条件(アクセスポリシー)をJSPに定義する。

  • アクセスポリシー定義例

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<!-- (1) -->
<sec:authorize access="hasRole('ADMIN')"> <!-- (2) -->
    <h2>Admin Menu</h2>
    <!-- omitted -->
</sec:authorize>

項番

説明

(1)
アクセスポリシーを適用したい部分を<sec:authorize>タグで囲む。
(2)
access属性にアクセスポリシーを定義する。ここでは、「管理者の場合は表示を許可する」というアクセスポリシーを定義している。

9.3.2.4.2. Webリソースに指定したアクセスポリシーとの連動

ボタンやリンクなど(サーバーへのリクエストを伴う画面項目)に対してアクセスポリシーを定義する際は、リクエスト先のWebリソースに定義されているアクセスポリシーと連動させる。
Webリソースに指定したアクセスポリシーと連動させる場合は、<sec:authorize>タグのurl属性を使用する。

url属性に指定したWebリソースにアクセスできる場合に限り<sec:authorize>タグの中に実装したJSPの処理が実行される。

  • Webリソースに定義されているアクセスポリシーとの連携例

<ul>
    <!-- (1) -->
    <sec:authorize url="/admin/accounts"> <!-- (2) -->
        <li>
            <a href="<c:url value='/admin/accounts' />">Account Management</a>
        </li>
    </sec:authorize>
</ul>

項番

説明

(1)
ボタンやリンクを出力する部分を<sec:authorize>タグで囲む。
(2)
<sec:authorize>タグのurl属性にWebリソースへアクセスするためのURLを指定する。
ここでは、「/admin/accountsというURLが割り振られているWebリソースにアクセス可能な場合は表示を許可する」というアクセスポリシーを定義しており、Webリソースに定義されているアクセスポリシーを直接意識する必要がない。

Note

HTTPメソッドによるポリシーの指定

Webリソースのアクセスポリシーの定義をする際に、HTTPメソッドによって異なるアクセスポリシーを指定している場合は、<sec:authorize>タグのmethod属性を指定して、連動させる定義を特定すること。

Warning

表示制御に関する留意点

ボタンやリンクなどの表示制御を行う場合は、必ずWebリソースに定義されているアクセスポリシーと連動させること。

ボタンやリンクに対して直接アクセスポリシーの指定を行い、Webリソース自体にアクセスポリシーを定義していないと、URLを直接してアクセスするような不正なアクセスを防ぐことができない。


9.3.2.4.3. 認可処理の判定結果を変数に格納

JSPにおいて、<sec:authorize>タグを使って呼び出した認可処理の判定結果は、変数に格納して使いまわすことができる。

  • JSPの実装例

<sec:authorize url="/admin/accounts"
                var="hasAccountsAuthority"/> <!-- (1) -->

<c:if test="${hasAccountsAuthority}"> <!-- (2) -->
    <!-- omitted -->
</c:if>

項番

説明

(1)
var属性に判定結果を格納するための変数名を指定する。
アクセスが許可された場合は、変数にtrueが設定される。
(2)
変数の値を参照して表示処理を実装する。

9.3.2.5. 認可エラー時のレスポンス

Spring Securityは、リソースへのアクセスを拒否した場合、以下のような流れでエラーをハンドリングしてレスポンスの制御を行う。

../_images/AuthorizationAccessDeniedHandling.png

認可エラーのハンドリングの仕組み

項番

説明

(1)
Spring Securityは、リソースやメソッドへのアクセスを拒否するために、AccessDeniedExceptionを発生させる。
(2)
ExceptionTranslationFilterクラスは、AccessDeniedExceptionをキャッチし、AccessDeniedHandlerまたはAuthenticationEntryPointインタフェースのメソッドを呼び出してエラー応答を行う。
(3)
認証済みのユーザーからのアクセスの場合は、AccessDeniedHandlerインタフェースのメソッドを呼び出してエラー応答を行う。
(4)
未認証のユーザーからのアクセスの場合は、AuthenticationEntryPointインタフェースのメソッドを呼び出してエラー応答を行う。

9.3.2.5.1. AccessDeniedHandler

AccessDeniedHandlerインタフェースは、認証済みのユーザーからのアクセスを拒否した際のエラー応答を行うためのインタフェースである。
Spring Securityは、AccessDeniedHandlerインタフェースの実装クラスとして以下のクラスを提供している。
Spring Securityが提供するAccessDeniedHandlerの実装クラス

クラス名

説明

AccessDeniedHandlerImpl
HTTPレスポンスコードに403(Forbidden)を設定し、指定されたエラーページに遷移する。
エラーページの指定がない場合は、HTTPレスポンスコードに403(Forbidden)を設定してエラー応答(HttpServletResponse#sendError)を行う。
InvalidSessionAccessDeniedHandler
InvalidSessionStrategyインタフェースの実装クラスに処理を委譲する。
このクラスは、CSRF対策とセッション管理機能を使用してセッションタイムアウトを検知する設定を有効にした際に、CSRFトークンがセッションに存在しない(つまりセッションタイムアウトが発生している)場合に使用される。
DelegatingAccessDeniedHandler
AccessDeniedExceptionAccessDeniedHandlerインタフェースの実装クラスのマッピングを行い、発生したAccessDeniedExceptionに対応するAccessDeniedHandlerインタフェースの実装クラスに処理を委譲する。
InvalidSessionAccessDeniedHandlerはこの仕組みを利用して呼び出されている。
RequestMatcherDelegatingAccessDeniedHandler

RequestMatcherインタフェースの仕組みを利用して、指定されたリクエストのパターンに対応するAccessDeniedHandlerインタフェースの実装クラスに処理を委譲する。

Note

RequestMatcherDelegatingAccessDeniedHandlerの設定方法については、リクエストパターン毎のセキュリティヘッダの出力DelegatingRequestMatcherHeaderWriterと同様にリクエストパターンの判定を行うRequestMatcherと処理を委譲するAccessDeniedHandlerを設定すれば良い。

なお、<sec:intercept-url>RequestMatcherDelegatingAccessDeniedHandlerがパスマッチングを行う間にはリクエストのパスが変わる可能性がある処理が挟まれないため、Warning「指定したパスが意図した通りに認識されない問題」に記載されているような事象は発生しない。

Spring Securityのデフォルトの設定では、エラーページの指定がないAccessDeniedHandlerImplが使用される。


9.3.2.5.2. AuthenticationEntryPoint

AuthenticationEntryPointインタフェースは、未認証のユーザーからのアクセスを拒否した際のエラー応答を行うためのインタフェースである。
Spring Securityは、AuthenticationEntryPointインタフェースの実装クラスとして以下のクラスを提供している。
Spring Securityが提供する主なAuthenticationEntryPointの実装クラス

クラス名

説明

LoginUrlAuthenticationEntryPoint
フォーム認証用のログインフォームを表示する。
BasicAuthenticationEntryPoint
Basic認証用のエラー応答を行う。
具体的には、HTTPレスポンスコードに401(Unauthorized)を、レスポンスヘッダとしてBasic認証用の「WWW-Authenticate」ヘッダを設定してエラー応答(HttpServletResponse#sendError)を行う。
DigestAuthenticationEntryPoint
Digest認証用のエラー応答を行う。
具体的には、HTTPレスポンスコードに401(Unauthorized)を、レスポンスヘッダとしてDigest認証用の「WWW-Authenticate」ヘッダを設定してエラー応答(HttpServletResponse#sendError)を行う。
Http403ForbiddenEntryPoint
HTTPレスポンスコードに403(Forbidden)を設定してエラー応答(HttpServletResponse#sendError)を行う。
HttpStatusEntryPoint
任意のHTTPレスポンスコードを設定して正常応答(HttpServletResponse#setStatus)を行う。
DelegatingAuthenticationEntryPoint
RequestMatcherインタフェースの仕組みを利用して、指定されたリクエストのパターンに対応するAuthenticationEntryPointインタフェースの実装クラスに処理を委譲する。

Spring Securityのデフォルトの設定では、認証方式に対応するAuthenticationEntryPointインタフェースの実装クラスが使用される。


9.3.2.5.3. 認可エラー時の遷移先

Spring Securityのデフォルトの設定だと、認証済みのユーザーからのアクセスを拒否した際は、アプリケーションサーバのエラーページが表示される。
アプリケーションサーバーのエラーページを表示してしまうと、システムのセキュリティを低下させる要因になるため、適切なエラー画面を表示することを推奨する。
エラーページの指定は、以下のようなbean定義を行うことで可能である。
  • SpringSecurityConfig.javaの定義例

@Bean("accessDeniedHandler")
public AccessDeniedHandler accessDeniedHandler() {
    LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> errorHandlers = new LinkedHashMap<>();

    // omitted

    AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
    defaultErrorHandler.setErrorPage("/WEB-INF/views/common/error/accessDeniedError.jsp"); // (1)

    return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
}

項番

説明

(1)
AccessDeniedHandlerをBean定義し、defaultHandlerに認可エラー用のエラーページを指定する。

Tip

サーブレットコンテナのエラーページ機能の利用

認可エラーのエラーページは、サーブレットコンテナのエラーページ機能を使って指定することもできる。

サーブレットコンテナのエラーページ機能を使う場合は、web.xml<error-page>タグを使用してエラーページを指定する。

<error-page>
    <error-code>403</error-code>
    <location>/WEB-INF/views/common/error/accessDeniedError.jsp</location>
</error-page>

9.3.3. How to extend

本節では、Spring Securityが用意しているカスタマイズポイントや拡張方法について説明する。

Spring Securityは、多くのカスタマイズポイントを提供しているため、すべてのカスタマイズポイントは紹介しない。
本節では代表的なカスタマイズポイントに絞って説明を行う。

9.3.3.1. 認可エラー時のレスポンス (認証済みユーザー編)

ここでは、認証済みユーザーからのアクセスを拒否した際の動作をカスタマイズする方法を説明する。


9.3.3.1.1. AccessDeniedHandlerの適用

Spring Securityが提供しているデフォルトの動作をカスタマイズする仕組みだけでは要件をみたせない場合は、AccessDeniedHandlerインタフェースの実装クラスを直接適用することができる。

例えば、Ajaxのリクエスト(REST APIなど)で認可エラーが発生した場合は、エラーページ(HTML)ではなくJSON形式でエラー情報を応答することが求められるケースがある。
そのような場合は、AccessDeniedHandlerインタフェースの実装クラスを作成してSpring Securityに適用することで実現することができる。
  • AccessDeniedHandlerインタフェースの実装クラスの作成例

public class JsonDelegatingAccessDeniedHandler implements AccessDeniedHandler {

    private final RequestMatcher jsonRequestMatcher;
    private final AccessDeniedHandler delegateHandler;

    public JsonDelegatingAccessDeniedHandler(
    public JsonDelegatingAccessDeniedHandler(
            RequestMatcher jsonRequestMatcher, AccessDeniedHandler delegateHandler) {
        this.jsonRequestMatcher = jsonRequestMatcher;
        this.delegateHandler = delegateHandler;
    }

    public void handle(HttpServletRequest request, HttpServletResponse response,
                      AccessDeniedException accessDeniedException)
           throws IOException, ServletException {
       if (jsonRequestMatcher.matches(request)) {
           // response error information of JSON format
           response.setStatus(HttpServletResponse.SC_FORBIDDEN);
           // omitted
       } else {
           // response error page of HTML format
           delegateHandler.handle(
                   request, response, accessDeniedException);
       }
   }

}
  • SpringSecurityConfig.javaの定義例

@Bean("accessDeniedHandler")
public JsonDelegatingAccessDeniedHandler accessDeniedHandler() {
    return new JsonDelegatingAccessDeniedHandler(accessAntPathRequestMatcher(), accessDeniedHandler()); // (1)
}

@Bean("accessAntPathRequestMatcher")
public AntPathRequestMatcher accessAntPathRequestMatcher() {
    return new AntPathRequestMatcher("/api/**");
}

@Bean("accessDeniedHandler")
public AccessDeniedHandler accessDeniedHandler() {
    AccessDeniedHandlerImpl bean = new AccessDeniedHandlerImpl();
    bean.setErrorPage("/WEB-INF/views/common/error/accessDeniedError.jsp");
    return bean;
}

@Bean
public SecurityFilterChain filterChain1(HttpSecurity http) {
    // omitted
    http.exceptionHandling(exceptionHandling -> exceptionHandling
            .accessDeniedHandler(accessDeniedHandler())); // (2)
    // omitted
    return http.build();
}

項番

説明

(1)

AccessDeniedHandlerインタフェースの実装クラスをbean定義してDIコンテナに登録する。

(2)

HttpSecurity#exceptionHandlingAccessDeniedHandlerのbeanを指定する。


9.3.3.2. 認可エラー時のレスポンス (未認証ユーザー編)

ここでは、未認証ユーザーからのアクセスを拒否した際の動作をカスタマイズする方法を説明する。


9.3.3.2.1. リクエスト毎にAuthenticationEntryPointを適用

認証済みユーザーと同様に、Ajaxのリクエスト(REST APIなど)で認可エラーが発生した場合は、ログインページ(HTML)ではなくJSON形式でエラー情報を応答することが求められるケースがある。
そのような場合は、リクエストのパターン毎にAuthenticationEntryPointインタフェースの実装クラスをSpring Securityに適用することで実現することができる。
  • SpringSecurityConfig.javaの定義例

@Bean("authenticationEntryPoint")
public DelegatingAuthenticationEntryPoint authenticationEntryPoint() {
    LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<>();
    map.put(antPathRequestMatcher(), entryPoint());
    DelegatingAuthenticationEntryPoint bean = new DelegatingAuthenticationEntryPoint(map); // (1)
    bean.setDefaultEntryPoint(defaultEntryPoint());
    return bean;
}

@Bean("antPathRequestMatcher")
public AntPathRequestMatcher antPathRequestMatcher() {
    return new AntPathRequestMatcher("/api/**");
}

@Bean("entryPoint")
public JsonAuthenticationEntryPoint entryPoint() {
    return new JsonAuthenticationEntryPoint();
}

@Bean("defaultEntryPoint")
public LoginUrlAuthenticationEntryPoint defaultEntryPoint() {
    return new LoginUrlAuthenticationEntryPoint("/login");
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    // omitted
    http.exceptionHandling(exceptionHandling -> exceptionHandling
            .authenticationEntryPoint(authenticationEntryPoint())); // (2)
    // omitted
    return http.build();
}

項番

説明

(1)
AuthenticationEntryPointインタフェースの実装クラスをbean定義してDIコンテナに登録する。
ここでは、Spring Securityが提供しているDelegatingAuthenticationEntryPointクラスを利用して、リクエストのパターン毎にAuthenticationEntryPointインタフェースの実装クラスを適用している。
(2)
HttpSecurity#exceptionHandlingAuthenticationEntryPointのbeanを指定する。

Note

デフォルトで適用されるAuthenticationEntryPoint

リクエストに対応するAuthenticationEntryPointインタフェースの実装クラスの指定がない場合は、Spring Securityがデフォルトで定義するAuthenticationEntryPointインタフェースの実装クラスが使用される仕組みになっている。

認証方式としてフォーム認証を使用する場合は、LoginUrlAuthenticationEntryPointクラスが使用されログインフォームが表示される。


9.3.3.3. ロールの階層化

認可処理では、ロールに階層関係を設けることができる。

上位に指定したロールは、下位のロールにアクセスが許可されているリソースにもアクセスすることができる。
ロールの関係が複雑な場合は、階層関係も設けることも検討されたい。
例えば、「ROLE_ADMIN」が上位ロール、「ROLE_USER」が下位ロールという階層関係を設けた場合、下記のようアクセスポリシーを設定すると、「ROLE_ADMIN」権限を持つユーザーは、/user配下のパス(「ROLE_USER」権限を持つユーザーがアクセスできるパス)にアクセスすることができる。
  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    // omitted
    http.authorizeHttpRequests(authz -> authz
            .requestMatchers(new AntPathRequestMatcher("/user/**")).hasAnyRole("USER")
            // omitted
            );
    // omitted
    return http.build();
}

9.3.3.3.1. 階層関係の設定

ロールの階層関係は、org.springframework.security.access.hierarchicalroles.RoleHierarchyインタフェースの実装クラスで解決する。

  • SpringSecurityConfig.javaの定義例

@Bean("roleHierarchy")
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl bean = new RoleHierarchyImpl(); // (1)
    bean.setHierarchy("""
            ROLE_ADMIN > ROLE_STAFF
            ROLE_STAFF > ROLE_USER
            """); // (2)
    return bean;
}

項番

説明

(1)
org.springframework.security.access.hierarchicalroles.RoleHierarchyImplクラスを指定する。
RoleHierarchyImplは、Spring Securityが提供するデフォルトの実装クラスである。
(2)
hierarchyプロパティに階層関係を定義する。

書式: [上位ロール] > [下位ロール]

上記例では、
STAFFは、USERに認可されたリソースにもアクセス可能である。
ADMINは、USERとSTAFFに認可されたリソースにもアクセス可能である。

9.3.3.3.2. Webリソースの認可処理への適用

ロールの階層化を、Webリソースと画面項目に対する認可処理に適用する方法を説明する。

  • SpringSecurityConfig.javaの定義例

@Bean
@Order(90)
public SecurityFilterChain filterChain(HttpSecurity http) {

    AuthorityAuthorizationManager<RequestAuthorizationContext> authManager = AuthorityAuthorizationManager.hasRole("STAFF");
    authManager.setRoleHierarchy(roleHierarchy()); // (1)
    // omitted
    http.authorizeHttpRequests(authz -> authz
            .requestMatchers(new AntPathRequestMatcher("/user/**"))
            .access(authManager) //(2)
            // omitted
            );

    return http.build();
}
項番
説明
(1)
org.springframework.security.authorization.AuthorityAuthorizationManagerのインスタンスを生成し、roleHierarchyプロパティにRoleHierarchyインタフェースの実装クラスのBeanを指定する。
(2)
(1)で生成したAuthorizationManageraccessメソッドで設定する。

9.3.3.3.3. メソッドの認可処理への適用

ロールの階層化を、Javaメソッドに対する認可処理に適用する方法を説明する。

  • SpringSecurityConfig.javaの定義例

@EnableMethodSecurity // (3)
@Configuration
public class SpringSecurityConfig {

    // (3)
    @Bean("methodExpressionHandler")
    public static MethodSecurityExpressionHandler methodExpressionHandler(
            RoleHierarchy roleHierarchy) {
        DefaultMethodSecurityExpressionHandler bean = new DefaultMethodSecurityExpressionHandler();  // (1)
        bean.setRoleHierarchy(roleHierarchy); // (2)
        return bean;
    }

項番

説明

(1)
org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandlerのBeanをstatic定義する。
(2)
roleHierarchyプロパティにRoleHierarchyインタフェースの実装クラスのBeanを指定する。
(3)
@EnableMethodSecurityアノテーションを設定する。

9.3.4. Appendix

9.3.4.1. Spring Securityのパスパターンマッチングにおける拡張子および末尾/の考慮

Spring MVCのパスパターンマッチングにおける拡張子および末尾/の考慮に記載している方法でパスパターンマッチを拡張している場合、RequestMatcherオブジェクトの設定が不足していると認可処理を行わずにアクセス出来てしまう。
パスパターンマッチの拡張箇所をカバーできるように、RequestMatcherの引数に”*“や**などのワイルドカード指定を含めている場合は問題とはならないが、そうでない場合はRequestMatcherの引数に対して個別に考慮する必要がある。
下記の設定例は、/restrictに対して「ROLE_ADMIN」ロールを持つユーザからのアクセスのみを許可している。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // omitted
    http.authorizeHttpRequests(authz -> authz
            .requestMatchers(new AntPathRequestMatcher("/restrict.*")).hasRole("ADMIN") // (1)
            .requestMatchers(new AntPathRequestMatcher("/restrict/")).hasRole("ADMIN") // (2)
            .requestMatchers(new AntPathRequestMatcher("/restrict")).hasRole("ADMIN") // (3)
    // omitted
    return http.build();
}

項番

説明

(1)
/restrictに拡張子を付けたパターン(/restrict.jsonなど)のアクセスポリシーを定義する。
個別に拡張子を許容している場合またはPathMatchConfigurer#setUseSuffixPatternMatch(true)を設定している場合は必須。
(2)
/restrictの末尾に”/“を付けたパターン(/restrict/)のアクセスポリシーを定義する。
個別に末尾の”/を許容している場合またはPathMatchConfigurer#setUseSuffixPatternMatch(true)を設定している場合は必須。
(3)
/restrictに対するアクセスポリシーを定義する。