最終更新日:2020/03/23 原本2019-09-09

Spring BootでWebセキュリティを設定しよう

Spring Bootで作るマイクロサービス 第7回

 前回、Spring Bootでのバックエンドでのデータ管理方法を紹介しました。今回も前回と同じサンプルアプリケーションを使って、管理者向けのWebアクセスに対してアクセス制限をかける方法を紹介します。

サンプルアプリケーションの概要

 サンプルアプリケーションは、図1に示す通り、郵便番号と住所データを提供する簡単なアプリケーションです。

 今回は管理者を想定したWeb側からのアクセスに対してBasic認証と任意のヘッダ値をチェックする2種類のセキュリティ設定を行う方法について説明します。

 通常、APIサーバの場合には、APIキーやAPIシークレットキーなどを用いてトークンをリクエストヘッダに設定するといった使い方が多いです。

図1:サンプルアプリケーションのイメージ
図1:サンプルアプリケーションのイメージ

Spring Security Web(1)

 Webでのセキュリティを扱う場合には、Filterなどを使って自分で作成することも可能ですが、セキュリティ関連のトレンドについていきながら安全なアプリケーションを維持するのは難しいことでもあります。

 しかし、Spring Security Webを使えば簡単に図2に示すようにWebアプリケーションにセキュリティ機能を追加できます。

 現在セキュリティ要件を満たしている場合でも、Spring Securityのようなフレームワークを使うことは、セキュリティのトレンドが変化しても対応しやすくなるためメリットがあります。

 また、Spring SecurityはWebセキュリティ機能以外のLDAPやOAuthなどもありますが、Web機能が最もよく利用されています。

図2:サンプルアプリケーションのイメージ
図2:サンプルアプリケーションのイメージ

 また、Spring Security Webには主に表1の機能があり、Spring Security Webを追加すると、これらの機能は自動的に有効になります。

表1:Spring Security Webの主な機能
主な機能 説明 
認証・認可 ログイン機能やロール設定などの機能があります。
セッション管理 セッションのライフサイクルや二重ログイン防止などの機能があります。
セキュリティ用ヘッダ frame-optionsなどセキュリティに関連するレスポンスヘッダをコントロールします。
CSRF対策機能 クロスサイトリクエストフォージェリ攻撃対策機能。
ファイアウォール機能 不正なURLなどのチェック機能。

Spring BootでのSpring Securityの設定

 Spring BootでSpring SecurityをGradleで使う場合には、リスト1のように「org.springframework.boot:spring-boot-starter-security」というスターターを追加します。

[リスト1]Spring Securityのスターター指定(build.gradleの抜粋)
dependencies {
    : // (省略)
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

基本的な使い方

 Spring BootでSpring Securityを使うには、リスト2のようにWebSecurityConfigurerAdapterクラスを継承して定義を行います。

 クラスを追加するだけで、既存のRestController側に何も記述せずにセキュリティ機能が追加できるため、セキュリティ部分とビジネス要件を完全に分離して管理できるのも利点です。

[リスト2]WebSecurityConfigurerAdapterクラスの継承例(src/main/java/com/coltware/springboot/zipcode/config/WebBasicAuthSecurityConfiguration.javaの抜粋)
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@Configuration
@EnableWebSecurity  //  (1) Spring Securityを使うための設定
public class WebBasicAuthSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        //  (2) 主に全体に対するセキュリティ設定を行う
        //  web.ignoring().antMatchers("/css/**","/js/**","/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //  (3) 主にURLごとに異なるセキュリティ設定を行う
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  (4) 主に認証方法の実装の設定を行う
    }
}

 まず、(1)のEnableWebSecurityアノテーションでSpring Securityを有効にします。WebSecurityConfigurerAdapterには3つの設定メソッドがあり、おおよその設定区分が異なっています。

 (2)のWebSecurityのconfigureメソッドでは全体に対するセキュリティ設定を行います。一般的によく行われる設定は、特定のパスのみセキュリティ設定を無効にするなどがあります。今回はAPIサーバなので静的リソースなどはありませんが、通常のWebアプリなどの場合にはCSSやJavaScriptファイル、画像ファイルなどをアクセス制限から除外する指定がよく行われます。

 (3)のHttpSecurityのconfigureメソッドではURLごとにセキュリティ設定を行います。複数のURLで異なるポリシーを設定する場合にはWebSecurityConfigurerAdapterインターフェースを実装したクラスを複数定義するのが簡単です。また、さまざまな記述方法がありますので、詳しくはSpring Securityのリファレンスなども参照してください。

 そして、(4)のAuthenticationManagerBuilderのconfigureメソッドでは、認証方法の実装方法などの設定を行います。

 これらの使い方は、後述するBasic認証での設定やヘッダでの認証で説明します。

Basic認証でのアクセス制限方法

 先ほどの基本的な使い方で紹介したメソッドに必要な実装を追加し、Basic認証を実装していきます。

 /admin以下のパスに対してBasic認証でアクセス制限を行うコードがリスト3のようになります。

[リスト3]Basic認証でのアクセス制御例(src/main/java/com/coltware/springboot/zipcode/config/WebBasicAuthSecurityConfiguration.javaの抜粋)
public class WebBasicAuthSecurityConfiguration extends WebSecurityConfigurerAdapter {

    // (1) Basic認証のID
    @Value("${zipcode.admin.username}")
    private String username;

    // (2) Basic認証のパスワード
    @Value("${zipcode.admin.password")
    private String password;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // (3) Basic認証の対象となるパス
        http.antMatcher("/admin/**");

        // (4) Basic認証を指定
        http.httpBasic();

        // (5) 対象のすべてのパスに対して認証を有効にする
        http.authorizeRequests().anyRequest().authenticated();

        // (6) すべてのリクエストをステートレスとして設定
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // : 省略
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // (7) Basic認証の実装を指定
        auth.authenticationProvider(new BasicAuthenticationProvider());
    }

    // (8) 認証処理の実装クラス
    public class BasicAuthenticationProvider implements AuthenticationProvider {

        // (9) 認証チェック
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {

            String name = authentication.getName();
            String password = authentication.getCredentials().toString();

            //  入力された name / password をチェックする
            if( name.equals(username) && password.equals(password) ){
               return new UsernamePasswordAuthenticationToken(name,password,authentication.getAuthorities());
            }

            throw new AuthenticationCredentialsNotFoundException("basic auth error");
        }

        // (10) 処理すべきAuthenticationクラスのチェック
        @Override
        public boolean supports(Class<?> authentication) {
            return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
        }
    }
    // : 省略
}

 (1)(2)で設定ファイルからBasic認証のIDとパスワードを取得するようにします。(3)ではアクセス制限をかけるパスのAnt形式でのパターンを指定しています。Ant形式以外にも、Spring MVC形式でのパターン指定のmvcMatcherなど、異なる指定方法があります。

 次に(4)のようにhttpBasic()にてBasic認証を行う指定をします。(5)では(3)で指定したすべてのパスに対して(4)の認証方法を行う設定にします。

 今回は、説明しやすいように(3)から(6)までの記述を1行ずつ記述していきましたが、これらを1行で記述していくことも可能です。

 ここでの記述がどのような設定となるのかがわかりにくい場合は、JavaがXML上ではどのようになるのかがわかるとよりわかりやすくなります。より詳しい内容は、Spring Securityのリファレンスを参照してください。

 (6)はセッション管理の指定ですが、Spring Securityではデフォルトでは自動的にセッションを作成してしまうため、APIサーバのような用途の場合には、ステートレスとして動作するように設定します。(7)で認証の実装を指定し、実際のBasic認証でのチェック処理はAuthenticationProviderインターフェースを実装したクラスを(8)のように実装します。

 (9)のauthenticateメソッドでは具体的なIDとパスワードをチェックする処理を実装し、(10)のsupportsメソッドは、AuthenticationProviderの実装クラスが処理すべきAuthenticationクラスかどうかをチェックしています。

 このように2つのメソッドを組み合わせて実装することで、他の認証形式の場合には処理を行わないようにし、複数の認証形式をサポートすることができるようになっています。

Spring Security Web(2)

特定ヘッダでのチェック

 続いて、特定のヘッダをチェックする方法を紹介します。

 APIの場合、図3に示すトークンを使ったアクセス制御はよく行う手法です。管理者からのアクセスに限らず、トークンに応じて柔軟なアクセスコントロールが行いやすいため、こちらの方法がより多く使われるのかもしれません。

 リスト4は、処理をより単純化し、X-Tokenという1つのヘッダ値でのチェックによって、アクセス制御を行うためのサンプルコードです。

特定ヘッダでのチェック
特定ヘッダでのチェック
[リスト4]特定ヘッダでのチェック例(src/main/java/com/coltware/springboot/zipcode/config/WebHeaderSecurityConfiguration.javaの抜粋)
public class WebHeaderSecurityConfiguration extends WebSecurityConfigurerAdapter {

    // : 省略

    // (1) フィルタを実装
    public class HeaderCheckFilter extends AbstractPreAuthenticatedProcessingFilter {

        private String headerName;

        public HeaderCheckFilter(String headerName) {
            this.headerName = headerName;
        }

        // (2) ヘッダ(X-Token)の値を返す
        @Override
        protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
            return request.getHeader(headerName);
        }

        @Override
        protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
            return "";
        }
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        HeaderCheckFilter filter = new HeaderCheckFilter("X-Token");

        // (3) ヘッダのチェック
        filter.setAuthenticationManager(new AuthenticationManager() {
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {

                String principal = (String) authentication.getPrincipal();

                // X-Tokenのヘッダの値をチェックする
                if(principal.equals("PASS")) {
                    // (4) 認証済みとして設定する
                    authentication.setAuthenticated(true);
                }
                else{
                    throw new BadCredentialsException("Token key error");
                }
                return authentication;
            }
        });

        // 認証の対象となるパス
        http.antMatcher("/admin/**");

        // (5) フィルタの設定
        http.addFilter(filter);

        // 対象のすべてのパスに対して認証を有効にする
        http.authorizeRequests().anyRequest().authenticated();

        // : 省略
    }

    //  : 省略
}

 (1)でヘッダから必要な値を取り出すためのフィルタのクラスを定義します。

 このクラスはAbstractPreAuthenticatedProcessingFilterインターフェースの実装クラスであり、getPreAuthenticatedPrincipalとgetPreAuthenticatedCredentialsのメソッドをオーバライドします。

 今回のサンプルでは(2)のように指定されたヘッダの値のみを返します。

 実際にヘッダのチェックは、(3)のようにsetAuthenticationManagerメソッド内で行い、ヘッダの値が問題なければ(4)のように認証済みとして設定します。

 後は先ほどのBasic認証と同様に設定していきますが、Basic認証を使う代わりに作成したフィルタを使うように(5)の通りに設定します。

 より自由に認証方法を実装したい場合には、今回の実装を参考にSpring Securityのリファレンスを参照するとよいと思います。

CSRF対策の設定

 CSRFとは、クロスサイトリクエストフォージェリの略であり、図4(左)のようにサイトアクセス者が不正サイトを通じて意図しない登録処理などを行う攻撃の1つです。

 この攻撃の一般的な防止法には、図4(右)のように、POSTやPUTなどのメソッドでそのページがリクエストをするときに、事前に作成したワンタイムパスワードと一緒にリクエストするといった方法があります。

 Spring Securityを有効にするとデフォルトでこのCSRF対策が有効になります。しかしながら、APIサーバではこの対策は必要ないためリスト5のように機能を無効にします。

図4:Spring SecurityでのCSRF対策
図4:Spring SecurityでのCSRF対策
[リスト5]CSRFの無効設定(src/main/java/com/coltware/springboot/zipcode/config/WebSecurityConfigurerAdapter.javaの抜粋)
@Override
protected void configure(HttpSecurity http) throws Exception {
    // : 省略
    http.csrf().disable();
    // : 省略
}

認証エラーの設定

 認証エラーが発生したときには、APIサーバであればリスト6のようにJSON形式でエラーを返す必要があります。

[リスト6]エラー時のJSON例
{
  "status": {
    "code": "error",
    "message": "Full authentication is required to access this resource"
  }
}

 その場合には、エラー時の処理をリスト6のように設定します。

[リスト7]エラーレスポンスの設定(src/main/java/com/coltware/springboot/zipcode/config/WebSecurityConfigurerAdapter.javaの抜粋)
@Override
protected void configure(HttpSecurity http) throws Exception {
    // : 省略

    // (1) エラーが生じたときのレスポンス設定
    http.exceptionHandling().authenticationEntryPoint(new ErrorAuthEntryPoint());
}
// (2) エラー結果を記述するための実装
public class ErrorAuthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {

        // (3) 実際のエラーを出力する
        ErrorResponse errorResponse = Response.createErrorResponse(authException);
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS,true);

        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");

        mapper.writeValue(response.getWriter(),errorResponse);
    }
}

 (1)で、認証エラーが生じた場合に、レスポンスを生成するクラスのインスタンスを設定します。

 レスポンスを生成するクラスは、(2)のようにAuthenticationEntryPointインターフェースを実装したクラスを作成します。

 実際のレスポンスは自由にであり、JSON形式でメッセージを出力するように(3)のようにjacksonライブラリのObjectMapperクラスを使って出力しています。

最後に

 Spring Securityは今回紹介した機能以外にも非常に多くの機能があり、通常のWebアプリケーションを想定したFormを使ったログインやログアウトなどの機能も備えてます。

 また、APIサーバでセキュリティをより強固にしたい場合にはOAuthなどのセキュリティ機能を使います。Spring SecurityはOAuthのプロバイダ機能もあるので、より強固なセキュリティにも対応可能です。

 しかし、機能が高度である一方、記述方法が柔軟であり、なかなか正しい制限方法を明確に記述することが難しくもあります。このあたりは、慣れが必要だと思うので、さまざまなサンプルを見て他の人の記述方法なども参考にしてみてください。

 今回まではサンプルアプリケーションを通じてSpring Bootでの各種スターター(ライブラリ等)の使い方を中心に紹介してきました。次回からはSpring Bootの仕組み側を見ていきます。

参考資料