SPA(Vue.js) + REST API構成でSpring Secirityで認証まで作った時に気を付けることや大事なこと


SPA(Vue.js) + REST API (Spring Boot)で認証処理を含んだアプリケーションを作成した際に、
大事だなと感じたところをまとめました。少しでも参考になれば幸いです。

# 開発前に事前知識として見ておいたほうがいいリンク
- [認証処理のアーキテクチャ](https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/Security/Authentication.html#id3)
- [オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
- [Preflight request (プリフライトリクエスト)](https://developer.mozilla.org/ja/docs/Glossary/Preflight_request)
- [Spring Securityのアーキテクチャ](https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/Security/SpringSecurity.html#springsecurityarchitecture)
- [セッション管理](https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/Security/SessionManagement.html#springsecuritysessionmanagement)
- [CSRF対策](https://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/Security/CSRF.html#springsecuritycsrf)

# Spring Securityで利用する各インターフェースの概要を押さえる
## 利用方法
実装クラスを独自にBeanに追加し利用する方法と、その他用意されているクラスも用いる方法がある。
詳細は、[spring.pleiades.io](https://spring.pleiades.io/)を確認する。

独自実装クラスを作成し利用する例を以下に示す。



```java:WebSecurityConfig
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint())
                .accessDeniedHandler(accessDeniedHandler());
    }

    @Bean
    AccessDeniedHandler accessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }
```


## AuthenticationProvider
主に、認証処理の実装を提供するためのインターフェースである。

## AuthenticationEntryPoint
未認証のユーザーが認証の必要なAPIにアクセスしたときの処理を行うためのインタフェースである。

## AccessDeniedHandler
ユーザーは認証済みだが未認可のリソースへアクセスしたときの処理を行うためのインタフェースである。

## AuthenticationSuccessHandler
認証成功後の処理を行うためのインターフェースである。

## AuthenticationFailureHandler
認証失敗後の処理を行うためのインターフェースである。

## LogoutSuccessHandler
ログアウト処理成功後の処理を行うためのインターフェースである。

# クライアント側での実装
- 毎回認証状態が維持できているかのヘルスチェックを、初期表示時や各処理終了後に行う。
- APIから返ってきたHTTPステータスコードを元に、リダイレクトの処理を行う。

バックエンド側は、クライアント側に関与しないため、HTTPステータスコードを返却するのみとする。

```javascript
    // ログイン処理
    const res = await axios.post('xxxxxxx', this.request)

    if(res.status === 200){
        // リダイレクト処理
    }
```

# バックエンド側での実装
- リダイレクトなどの処理はCORSが絡むためクライアント側へ委譲し、HTTPステータスコードの返却のみを行う。

```java:CustomAuthenticationFailuerHandler
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {

        // 簡素な例だが、HTTPステータスコードの返却のみを行っている
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
    }
```

# 既知のFilterクラスの1つ前に独自のFilterを追加する
```java:WebSecurityConfig
http.addFilterBefore(javax.servlet.Filter filter,
                  java.lang.Class<? extends javax.servlet.Filter> beforeFilter)
```
第一引数のfilterには、独自に通したいフィルターを
第二引数のbeforeFilterには、既知のフィルターを記述する。
 
以下の例では、UsernamePasswordAuthenticationFilterの前に、authenticationFilter()を追加している。  

```java:WebSecurityConfig
    @Bean
    CustomAuthenticationFilter authenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        filter.setRequiresAuthenticationRequestMatcher(
                new AntPathRequestMatcher("/login", "POST"));
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(authenticationFailuerHandler());
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 独自のauthenticationFilterをUsernamePasswordAuthenticationFilterの前に通過するように追加
        http.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
```

# CORS対策処理の追加
SPA + REST API構成のアプリケーションでは、CORSが未対策の場合、間違いなく引っかかるかと思うので以下対策が必要です。
CorsConfigurationの詳細は、[こちら](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html)を参照してください。

```java:WebSecurityConfig

      private CorsConfigurationSource corsFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true);
        configuration.addAllowedOrigin("xxxxx");
        configuration.addAllowedMethod(CorsConfiguration.ALL);
        configuration.addAllowedHeader(CorsConfiguration.ALL);
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS", "DELETE", "PUT"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CORS対策
        http.cors().configurationSource(this.corsFilter());
    }
```