【Spring Security】認証エラー時に独自のJSONを返す方法

やりたいこと

Spring MVCで作成したRest APIに対するリクエストが認証エラーとなった際に独自のJSONをレスポンスとして返すようにします。

認証はリクエストのAuthorizationヘッダ中のトークンを使用しリクエストごとに認証を行います。この認証方法は別記事にしています。

実装方法の概要

実装のポイントは以下の3点です。

  1. エラーレスポンスを表すクラスを作成する。
  2. AuthenticationEntryPointインターフェイスを実装したクラスをBean登録する。
  3. Spring SecurityのauthenticationEntryPointに2.で作成したBeanを指定する。

エラーレスポンスクラスの作成

エラーレスポンスとして返したいプロパティを持つクラスを定義します。ここではエラーコードとエラー内容を持つ以下のクラスを定義します。ゲッター、セッター、コンストラクタはLombokで生成します。

// importは省略

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {

    private String errorCode;
    private String message;
}

AuthenticationEntryPointインターフェイスの実装クラスのBean定義

AuthenticationEntryPointインターフェイスを実装したクラスのBean定義を行います。

AuthenticationEntryPointのcommenceメソッド中で引数のresponseに対してエラーコード、Content type、レスポンスボディを設定します。レスポンスボディが作成したエラーレスポンスクラスのJSONとするためにエラーレスポンスクラスをObjectMapperを使ってJSON文字列へ変換しています。

AuthenticationEntryPointは抽象メソッドが1つなのでBean定義はラムダ式を使って以下のように書くことができます。

    @Bean
    public AuthenticationEntryPoint unauthorizedEntryPoint() {

        // エラーレスポンスクラスをJSONへ変換するために使う。
        var objectMapper = new ObjectMapper();

        return (request, response, authException) -> {
            // ステータスコードを401とする。
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

            // Content typeをapplication/jsonとする。
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);

            // レスポンスボディをエラーレスポンスクラスのJSONとする
            response.getOutputStream().println(objectMapper.writeValueAsString(new ErrorResponse("unauthorized", "Invalid access token")));
        };
    }

AuthenticationEntryPointへの登録

作成したBeanをSpring SecurityのAuthenticationEntryPointへ登録します。

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity()
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(preAuthenticatedProcessingFilter())
                .exceptionHandling()
                .authenticationEntryPoint(unauthorizedEntryPoint()) // ここで登録
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable()
                .cors();
    }

動作確認

ローカルで起動しcurlでリクエストを送って動作確認を行います。Authorizationヘッダにエラーとなる値を設定します。以下のように設定した値となっていることがわかります。

$ curl -H 'Authorization:key22' -i  http://localhost:8080/items/1\?name\=111
HTTP/1.1 401 ←ステータスコードが401となっている
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
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: application/json ←Contet typeがapplication/jsonになっている
Content-Length: 63
Date: Fri, 15 Jan 2021 13:25:47 GMT

{"errorCode":"unauthorized","message":"Invalid access token"} ←作成したエラーレスポンスクラスのJSONとなっている。