[Spring Security] 事前認証シナリオ用に提供されたクラスを元に、API キーによる認証認可の実装例を用意・解説した。

#はじめに

##なぜ記事を書いたか?

直近で携わったプロダクトでは、Spring Securityを用いて、**APIキーによる認証・認可** を実装しました。

詳しくは後述しますが、Spring Security を通じた **APIキーによる認証・認可** は、公式から特に実装例等は公開されていないため、


> ![testtest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287824/9cb317e2-950a-5c3d-74e5-1751fbde5cfa.png)
> 公式ドキュメント: [事前認証シナリオ](https://spring.pleiades.io/spring-security/site/docs/5.0.1.RELEASE/reference/html/preauth.html)より引用

実装する際、`公式ドキュメント` と `Spring Securityのコード` を反復しながら、認証・認可の流れを体系的を理解し、コードに落とし込むという進め方をしました。

上記の進め方の難点は、「体系的に理解をするハードルが高い」ということです。
Github内検索や、Google検索にて `site:github.com` 句を多様しつつ、各クラスを転々と読み込みながら、処理の流れを体系的に理解するということは、なかなか骨が折れます。

個人的に、処理の流れを体系的な理解をする際は、ミニマムな実装例を用意し、その実装例を元に、公式ドキュメントを読んでいく進め方が効率が良いと感じています。

今回はこのような背景から、**Spring Security を通じた、 APIキーによる認証・認可認証・認可の流れを体系的を理解するためのミニマムな実装例と、解説**を記事として用意しました。

この記事が、同様のプロジェクトに着手する開発者にとって、体系的な理解を助けるものとなれたら幸いです。

なお、ソースは全て[Github:y-tomimoto/spring-security-demo](https://github.com/y-tomimoto/spring-security-demo)から閲覧できます。

##想定読者

- Spring Boot でアプリケーションを構築したことがあり、今後Spring Security にも触れたいと思っている人
- Spring Boot で Spring Security を導入済みだが、どのように動作しているのか把握できている自信がない人
- Spring Security で認証方式を実装したことがあるが、応用する自信がない人
- Spring Security を体系的に理解したい人

##Spring Security で提供されている認証方式について

Spring Security では、いくつかの認証方式がデフォルトでサポートされており、各方式毎に、それらを容易に実現するクラスが提供されています。

- [フォームログイン](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-authentication-form) => [UsernamePasswordAuthenticationFilter](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.html)
- [BASIC認証](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-authentication-basic) => [BasicAuthenticationFilter](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.html)
- etc ...

##APIキーによる認証方式について

ただし、APIキーによる認証方式は、上記の通りではありません。

APIキーによる認証方式専用のクラスは提供されていない(※1)ため、Spring Security を 体系的に理解した上で、提供されている複数のクラスを適切に組み合わせて認証を実現する必要があります。


> (※1) `APIキーによる認証方式専用のクラスは提供されていない`: 改めて後述しますが、APIキーによる認証方式は、Spring Security では[事前認証シナリオ](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-preauth)のケースに該当します。Spring Securityでは、事前認証用に提供されているクラス群が存在するため、そのクラス群を活用する必要があります。なお、以下に記載の通り、事前認証シナリオを実装する際、概要のみが公開されており、実装に関しては、自身でソースを読みながら実装を進める必要があります。

> ![testtest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287824/9cb317e2-950a-5c3d-74e5-1751fbde5cfa.png)
> 公式ドキュメント: [事前認証シナリオ](https://spring.pleiades.io/spring-security/site/docs/5.0.1.RELEASE/reference/html/preauth.html)より再掲

# 記事の構成
1. 前提知識
2. [APIキーによる認証の実装例](#2-apiキーによる認証の実装例)
3. [認証の実装例の解説](#3認証の実装例の解説)
4. [APIキーによる認証・認可の実装例](#4apiキーによる認証認可の実装例)
5. [認可の実装例の解説](#5認可の実装例の解説)
6. [最後に](#最後に)

<details><summary>1.前提知識はこちら</summary><div>


ここでは、Spring Securityを初めて触る方に向けて、**抽象的に**Spring Securityの仕組みをおさらいしています。

既にSpring Securityがどのように動作するかイメージできている場合は、[Skip](#2-apiキーによる認証の実装例)してください。

## Spring Security での大まかな認証の流れ

実装例を解説する前に、認証がどのように行われるのかを、かんたんにおさらいしてみます。



Spring Bootで提供されている各認証方式では、大きく以下の2つのフローを実行しています。



1. **リクエストから認証情報を取得**
2. **その認証情報が正しいものか確認**



これら2つの認証フローは、各認証方式毎に用意された `AuthenticaitonFilter` と名の付くクラス([※2](#2-authenticaitonfilterと名の付くクラスを実装するための基底クラスインターフェースは複数ありますそしてこれらを元に実装されたauthenticaitonfilterクラス内の認証フローに冠する処理は共通している部分が多いです))で実行されています。

それでは、`AuthenticaitonFilter` クラス内で、

1. **リクエストから認証情報を取得**
2. **その認証情報が正しいものか確認**

という2つのフローをどのように実行しているか、具体的に確認していきます。

---

 `AuthenticaitonFilter` と名の付くクラス内では、おおまかに下記のような処理を行っています。



1. [認証フロー] **リクエストから認証情報を取得**

    

    1. [AuthenticaitonFilter] リクエストから認証情報を抽出する
    2. [AuthenticaitonFilter] 抽出した認証情報を元に `AuthenticationToken` を作成する



2. [認証フロー] **その認証情報が正しいものか確認**

    

    3. [AuthenticaitonFilter] 生成した `AuthenticationToken` を用いて `AuthenticationManager` が認証処理を実行する
    4. [AuthenticaitonFilter] 認証の確認が正常に終了する

    

---

ここで初めて登場した

- `AuthenticationToken`
- `AuthenticationManager` 

について簡単に説明します。



**AuthenticationToken**

リクエストには、様々な形式で認証情報が含まれています。

- Authorization Header として認証情報が含まれているケース
- Postパラメータとして認証情報が含まれているケース
- etc...

抽出したこれらの認証情報が格納し、以降の認証処理で参照するためのオブジェクトが `AuthenticationToken` です。



**AuthenticationManager**

`AuthenticationManager` には、**認証情報が正しいかどうかを確認するための認証処理が格納** されています。



抽象的ではありましたが、Spring Security内での認証のフローは以上のようになります。



> ###### (※2) `AuthenticaitonFilter`と名の付くクラスを実装するための**基底クラス・インターフェース**は複数あります。そして、これらを元に実装されたAuthenticaitonFilterクラス内の認証フローに冠する処理は共通している部分が多いです。
>> **`AuthenticaitonFilter` を実装するための基底クラス・インターフェース**:
[AbstractAuthenticationProcessingFilter](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html),[OncePerRequestFilter](https://spring.pleiades.io/spring-framework/docs/5.0.x/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html),[AbstractPreAuthenticatedProcessingFilter](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.html)


##前提知識のまとめ

Spring Securityでは、`AuthenticaitonFilter` と名の付くクラスで、以下の手順で認証を行っています。

1. リクエストから認証情報を抽出する
2. 抽出した認証情報を元に `AuthenticationToken` を作成する
3. 生成した `AuthenticationToken` を用いて `AuthenticationManager` が認証処理を実行する
4. 認証の確認が正常に終了する



抽象的に認証の流れをおさらいしたところで、
実際にAPIキーによる認証の実装例を確認してみましょう。
</div></details>

#2. APIキーによる認証の実装例

以下はSpring SecurityでAPIキーによる認証を実装するミニマムな構成です。

認証・認可のうち、このbranchでは、認証のみを実装しています。

> 認証と認可の違いについて: https://dev.classmethod.jp/articles/authentication-and-authorization-again/

## [Repository](https://github.com/y-tomimoto/spring-security-demo/tree/authentication)

- [プロジェクトのベースとなっている Spring Initializr の設定](https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.5.3.RELEASE&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=web,security,lombok)
- 上記は、Spring Initializr のデフォルト設定に、Dependencyとして Spring Web と Spring Security と Lombok を追加しただけのもの

##ディレクトリ構成

```
demo
├── DemoApplication.java
├── controller
│   └── HelloWorldController.java
└── security
    ├── config
    │   └── MyWebSecurityConfigurer.java
    ├── exception
    │   └── handler
    │       └── authentication
    │           └── MyAuthenticationFailureHandler.java
    ├── filter
    │   └── MyPreAuthenticatedProcessingFilter.java
    ├── model
    │   └── MyUserDetails.java
    └── service
        └── MyAuthenticationUserDetailsService.java

```

##用意したクラス

### HelloController.java

```java
package com.example.demo.controller;

...

@RestController
public class HelloWorldController {
  @GetMapping("/hello")
  String hello() {
    return "Hello";
  }
  @GetMapping("/world")
  String world() {
    return "World";
  }
}
```

###MyWebSecurityConfigurer.java

```java
package com.example.demo.security.config;

...
@Configuration
@EnableWebSecurity
class MyWebSecurityConfigurer extends WebSecurityConfigurerAdapter {

  @Override
  public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/error");
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilter(getMyPreAuthenticatedProcessingFilter());
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(getPreAuthenticatedAuthenticationProvider());
  }

  public AbstractPreAuthenticatedProcessingFilter getMyPreAuthenticatedProcessingFilter() throws Exception {
    var myPreAuthenticatedProcessingFilter = new MyPreAuthenticatedProcessingFilter();
    myPreAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager());
    myPreAuthenticatedProcessingFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
    myPreAuthenticatedProcessingFilter.setCheckForPrincipalChanges(true);
    myPreAuthenticatedProcessingFilter.setInvalidateSessionOnPrincipalChange(true);
    return myPreAuthenticatedProcessingFilter;
  }

  public PreAuthenticatedAuthenticationProvider getPreAuthenticatedAuthenticationProvider() {
    var preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider();
    preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new MyAuthenticationUserDetailsService());
    return preAuthenticatedAuthenticationProvider;
  }
}


```


###MyPreAuthenticatedProcessingFilter.java

```java
package com.example.demo.security.filter;

...

public class MyPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {

  @Override
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).orElse("");
  }

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

}

```

###MyAuthenticationUserDetailsService.java

```java
package com.example.demo.security.service;

...

public class MyAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

  @Override
  public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {

    var myUserDetails = new MyUserDetails();

    var key = token.getPrincipal().toString();

    /**
     * Check API Key
     */
    if (!(key.equals("hello") || key.equals("world"))) {
      throw new UsernameNotFoundException("Invalid key.");
    }

    myUserDetails.setKey(key);

    return myUserDetails;
  }
}
```

###MyUserDetails.java

```java
package com.example.demo.security.model;

...

public class MyUserDetails implements UserDetails {

  private String key;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
  }

  @Override
  public String getPassword() {
    return null;
  }

  @Override
  public String getUsername() {
    return key;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}

```

##MyAuthenticationFailureHandler

```java
package com.example.demo.security.exception.handler.authentication;

...

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    /**
     * API Key Reset Link
     */
    response.sendRedirect("https://www.google.com");
  }
}
```

##認証情報が含まれている箇所

今回は、`Authorization` header に認証情報が含まれているものとします。

```shell
curl --location --request GET 'localhost:9800/hello' \
--header 'Authorization: hello'
```

#3.認証の実装例の解説

##MyWebSecurityConfigurer.java

まずは、`MyWebSecurityConfigurer.java` について解説します。

```java
package com.example.demo.security.config;

...
@Configuration
@EnableWebSecurity 
class MyWebSecurityConfigurer extends WebSecurityConfigurerAdapter { ・・・①
  
  @Override
  public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/error"); ・・・②
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilter(getMyPreAuthenticatedProcessingFilter()); ・・・③
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(getPreAuthenticatedAuthenticationProvider()); ・・・④
  }

  public AbstractPreAuthenticatedProcessingFilter getMyPreAuthenticatedProcessingFilter() throws Exception {
    var myPreAuthenticatedProcessingFilter = new MyPreAuthenticatedProcessingFilter(); 
    myPreAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager()); ・・・⑤
    myPreAuthenticatedProcessingFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); ・・・⑥
    myPreAuthenticatedProcessingFilter.setCheckForPrincipalChanges(true); ・・・⑦
    myPreAuthenticatedProcessingFilter.setInvalidateSessionOnPrincipalChange(true); ・・・⑦
    return myPreAuthenticatedProcessingFilter;
  }

  public PreAuthenticatedAuthenticationProvider getPreAuthenticatedAuthenticationProvider() {
    var preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider();
    preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new MyAuthenticationUserDetailsService()); ・・・⑧
    return preAuthenticatedAuthenticationProvider;
  }
}


```

###① `EnableWebSecurity` アノテーションと `WebSecurityConfigurerAdapter` クラスについて

`@Configuration` アノテーションが付与されている `WebSecurityConfigurer` インターフェースの実装クラスに、`@EnableWebSecurity` アノテーションを付与することで、実装クラス内に記載した**任意の認証方式**([※3]())をSpring Boot アプリケーションに適用することができます。



Spring Security には `WebSecurityConfigurerAdapter` という、**任意の認証方式**を実装のするためのクラスが用意されています。

`WebSecurityConfigurer` インターフェースを自ら実装するのではなく、`WebSecurityConfigurer` インターフェースを実装した `WebSecurityConfigurerAdapter` クラスを拡張し、任意の認証方式([※3]())を実装するのが一般的です。このことは、Spring Securityのコード内にもコメントとして記載されています。


> [ソースコード内のコメント](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java#L31)より: Add this annotation to an @Configuration class to have the Spring Security configuration defined in any WebSecurityConfigurer or more likely by extending the  WebSecurityConfigurerAdapter base class and overriding individual methods



今回はセオリーに乗っ取り、`@Configuration` アノテーションが 付与されている `WebSecurityConfigurerAdapter` クラスを継承したクラスに、`@EnableWebSecurity` アノテーション を付与しています。

> ###### (※3)**任意の認証方式**と**デフォルトの認証方式**: 今回Spring Securityを利用するために、Spring Initializer を通じて下記をdependencyに追加しています。<br><br>```implementation 'org.springframework.boot:spring-boot-starter-security'```<br><br>この段階で、Spring Boot アプリケーション内に、`@EnableWebSecurity` アノテーションが付与された`WebSecurityConfigurer` インターフェースの実装クラスが存在しない場合、デフォルトの認証方式が有効になります。デフォルトの認証方式については後述します。

###②ignoreについて

WebSecurityを引数に取るconfigureメソッドでは、主に認証外とするリソースを宣言します。
ここで `/error` パスを認証外としているのには理由があります。

上記の`EnableAutoConfiguration`により、[Spring MVCエラーコントローラー](https://spring.pleiades.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.html)が自動構成されます。この機能により、内部エラーは全て `/error` へリダイレクトされます。

<br>

このとき、`/error` パスが認証対象となっていると、そのリダイレクト自体にも認証処理が実行されてしまい、Spring Securityの動作を把握しづらくなってしまいます。

そのため、運用・開発上の観点から、上記をignore対象としています。


###③PreAuthenticatedProcessingFilterを追加する

```java
    http.addFilter(getMyPreAuthenticatedProcessingFilter()); ・・・③
```

認証処理は、AuthenticationFilterという名のつくクラス内で実行されます。

今回はAPIキーによる認証方式を採用するので、`AbstractPreAuthenticatedProcessingFilter`インターフェースの実装クラス、`MyPreAuthenticatedProcessingFilter`を追加(※4)しています。



> (※4)なぜ `AbstractPreAuthenticatedProcessingFilter` の実装クラスを追加しているのか? 
>>　HOT PEPPERは、お店を検索するための [`グルメサーチAPI`](https://webservice.recruit.co.jp/doc/hotpepper/reference.html)を公開しています。このAPIを利用するためには、メールアドレスとパスワードを用いて会員登録する必要があります。会員登録後、ユーザはAPIキーを発行され、そのAPIキーをリクエストに含めることで、グルメサーチAPIを利用することができます。<br><br>このように、サービスの利用にAPIキーを要するサービスは、**APIキーの発行前に、事前にユーザに対してなんらかの認証を課している**ケースがほとんどです。<br><br>(事実、HOT PEPPERではAPIキーを不特定多数に発行しているのではなく、有効なメールアドレスを保持するユーザに対してのみ発行を行っていると言えます。)<br><br>このように、**ユーザがAPIキーを保持している時点で、そのユーザは、なんらかの形式で、サービスの主体から認証済みである**と捉えることができます。<br><br>よって、APIキーによる認証は、Spring Securityで掲げる、[**事前認証シナリオ**](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-preauth)に該当すると考えられます。事前認証シナリオに該当する場合に、利用が推奨されているFilterこそ、`AbstractPreAuthenticatedProcessingFilter`です。

### ③-1 `MyPreAuthenticatedProcessingFilter` はどこに **addFilter** されるのか?

addFilter句について解説する前に、サーブレットフィルターについておさらいします。

Spring Securityは、[**サーブレットフィルター**の仕組みの上で構成](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-architecture)されています。



####サーブレットフィルター

[サーブレットフィルター](https://docs.oracle.com/cd/E18355_01/web.1013/B31859-01/filters.htm)には、リクエストの前処理やサーバー・レスポンスの後処理を記載することができます。
Webアプリケーション内の[複数のエンドポイントに対して、共通の処理を追加したい](https://docs.oracle.com/cd/E18355_01/web.1013/B31859-01/develop.htm#409407)ときに有効です。


_Filterをイメージするための例_

```java

  public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
    throws ServletException,IOException{
    // リソースにリクエストが到達する前にリクエストを加工する
    request.setCharacterEncoding("UTF-8");

    // 次のFilterを実行する or
    // 自身が最後のFilterなら、本来リクエストが投げられるはずのエンドポイントにリクエストする
    chain.doFilter(request,response);
    
    // リソースから帰ってきたレスポンスを加工する
    response.setContentType("application/json");
  }
```


> 共通の処理の例: リクエストやレスポンスの暗号化、認証、ロギング、監査、データ圧縮およびキャッシュ

Spring Security も例に漏れず、複数のエンドポイントに対して `認証` という処理を追加する役割を担うので、サーブレットフィルターの仕組みの上で構築されています。

####デフォルトで構成されるSecurity Filter

では早速、Spring Securityを構成するサーブレットフィルターの中身を確認してみましょう。

Spring Securityを利用するにあたり、Spring Initializerを通じて通じて、依存関係に下記を追加しています。

```gradle
	implementation 'org.springframework.boot:spring-boot-starter-security'
```

Spring Boot アプリケーション内に、`@EnableWebSecurity` アノテーションが付与された`WebSecurityConfigurer` インターフェースの実装クラスが存在しない場合、Spring Securityはデフォルトの認証方式を自動構成します。

デフォルトで追加されるSpring Securityを構成するサーブレットフィルターにより、いくつかの機能が有効になります。

Spring Bootを起動するとき、デフォルトで構成されるSpring Securityを構成するサーブレットフィルターがコンソールのログから確認できます。

> tips: application.propertiesに `logging.level.org.springframework.security=TRACE
` を配置することで、Spring Securityの流れがより可視化されます。

```java
INFO 177 --- [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [

  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@2f84acf7, 
  org.springframework.security.web.context.SecurityContextPersistenceFilter@712cfb63, 
  org.springframework.security.web.header.HeaderWriterFilter@36681447, 
  org.springframework.security.web.csrf.CsrfFilter@50850539, 
  org.springframework.security.web.authentication.logout.LogoutFilter@7e642b88, 
  org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@671d1157, 
  org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6b350309, 
  org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@291373d3, 
  org.springframework.security.web.authentication.www.BasicAuthenticationFilter@618ff5c2, 
  org.springframework.security.web.savedrequest.RequestCacheAwareFilter@15639440, 
  org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@151bf776, 
  org.springframework.security.web.authentication.AnonymousAuthenticationFilter@372ca2d6, 
  org.springframework.security.web.session.SessionManagementFilter@726aa968, 
  org.springframework.security.web.access.ExceptionTranslationFilter@4d95a72e, 
  org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6f94fb9d

]
```

> デフォルトで構成される機能一覧は、[公式ドキュメント](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-hello-auto-configuration)からも確認することができます。

####任意の認証方式を構成するためのSecurity Filter

今回は、任意の認証方式を実現するために、`@EnableWebSecurity` でアノテートされた`WebSecurityConfigurerAdapter` を拡張したクラスを用意しています。



`@EnableWebSecurity` でアノテートされた`WebSecurityConfigurerAdapter` を拡張したクラスでは、任意の認証方式の土台となるサーブレットフィルターをデフォルトで構成します。

土台となるサーブレットフィルターには、今後追加される任意の認証方式を受け入れるため、認証部分を実行するサーブレットフィルターは含まれておらず、自動構成のときよりも適用されているサーブレットフィルターが少なくなっています。

```java
INFO 274 --- [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
  
  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@642505c7, 
  org.springframework.security.web.context.SecurityContextPersistenceFilter@40e60ece, 
  org.springframework.security.web.header.HeaderWriterFilter@5fac521d, 
  org.springframework.security.web.csrf.CsrfFilter@782bf610, 
  org.springframework.security.web.authentication.logout.LogoutFilter@476e8796, 
  org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3a230001, 
  org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4d654825, 
  org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4339e0de, 
  org.springframework.security.web.session.SessionManagementFilter@706eab5d, 
  org.springframework.security.web.access.ExceptionTranslationFilter@1846579f

]

```

ここでようやく 

Q.`MyPreAuthenticatedProcessingFilter` はどこに**addFilter**されるのか? 

について回答することができます。

`MyPreAuthenticatedProcessingFilter` は下記に**add**されます。

```java

  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@642505c7, 
  org.springframework.security.web.context.SecurityContextPersistenceFilter@40e60ece, 
  org.springframework.security.web.header.HeaderWriterFilter@5fac521d, 
  org.springframework.security.web.csrf.CsrfFilter@782bf610, 
  org.springframework.security.web.authentication.logout.LogoutFilter@476e8796, 

+ com.example.demo.security.filter.MyPreAuthenticatedProcessingFilter@XXXXXXXX,

  org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3a230001, 
  org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4d654825, 
  org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4339e0de, 
  org.springframework.security.web.session.SessionManagementFilter@706eab5d, 
  org.springframework.security.web.access.ExceptionTranslationFilter@1846579f

```

なぜこの位置に配置されるのかについて解説したいと思います。

Spring Securityでは、`FilterComparator` クラスにて、**Spring Securityを構成するサーブレットフィルター**(以下Security Filterと呼ぶ)の構成順序を各Security Filterのインターフェースにより管理しています。


```java
	FilterComparator() {
		int order = 100;
		put(ChannelProcessingFilter.class, order);
		order += STEP;
		put(ConcurrentSessionFilter.class, order);
		order += STEP;
		put(WebAsyncManagerIntegrationFilter.class, order);
		order += STEP;
		put(SecurityContextPersistenceFilter.class, order);
		order += STEP;
		put(HeaderWriterFilter.class, order);
		order += STEP;
		put(CsrfFilter.class, order);
		order += STEP;
		put(LogoutFilter.class, order);
        ...
```

> FilterComparator: https://github.com/spring-projects/spring-security/blob/ae6af5d73ce77f93926379f96371e84b1e9401f1/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java#L61

`AbstractPreAuthenticatedProcessingFilter` インターフェースも、Comparator内で位置が管理されています。

```java
		put(LogoutFilter.class, order);
		order += STEP;
		put(X509AuthenticationFilter.class, order);
		order += STEP;

		put(AbstractPreAuthenticatedProcessingFilter.class, order);
		order += STEP;

		filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",order);
		order += STEP;
		put(UsernamePasswordAuthenticationFilter.class, order);
		order += STEP;
		put(ConcurrentSessionFilter.class, order);

```

`MyPreAuthenticatedProcessingFilter` は `AbstractPreAuthenticatedProcessingFilter` インターフェースで実装されているため、addFilter句を用いてSecurity Filterを追加した場合、Comparator内で管理されているインターフェースと一致する位置にFilterが挿入されます。

他のSecurity Filterもこの順序どおりに配置されている(※5)ので、気になる方は確認してみてください。

> (※5)**なぜ位置が決まっているのか?**: AuthenticationFilterを実行するにあたり、Filterによっては、AuthenticationFilterより前のFilterで生成されている特定のObjectが必要になるケースもあります。このように、Filterによって事前に作成されているべきObjectがあったり、直後のFilterの動作を監視し、その結果によって処理を行うようなFilterもあります。このような理由から、それぞれのFilterには、固定の位置があるのです。
>> AbstractAuthenticationProcessingFilterの場合: [... 認証が成功した場合、結果の Authentication オブジェクトは現在のスレッドの SecurityContext に配置されます。これは、以前のフィルターによってすでに作成されていることが保証されています。
](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html)

###④AuthenticationProviderをsetする

```java
    auth.authenticationProvider(getPreAuthenticatedAuthenticationProvider()); ・・・④
```

抽象的にSpring Securityにおける認証の流れをおさらいしたセクションにて、

> 生成した `AuthenticationToken` を用いて `AuthenticationManager` が認証処理を実行する

という節がありました。

先程は抽象的に解説しましたが、実際に認証処理を行うのは、`AuthenticationManager` ではありません。

実際に認証処理を行うのは、`AuthenticationProvider` です。



ここでは、`AuthenticationToken` を受け取る `AuthenticationManager` に対して、`PreAuthenticatedAuthenticationProvider` をsetしています。



`AbstractPreAuthenticatedProcessingFilter` 内で生成された `AuthenticationToken` は、このProvider内で認証処理に利用されます。

>  `AuthenticationManager` と `AuthenticationProvider` の関係について
>> `AuthenticationManager` 内には、複数の `AuthenticationProvider` を保持できるようになっています。受け取った `AuthenticationToken` を用いて実際の認証処理を実行するのは、`AuthenticationManager` に外部からsetされた `AuthenticationProvider` です。`AuthenticationManager` は複数の認証処理を束ね、受け取ったTokenを適切なProviderに流す役割を担っています。


###④-1: なぜ `PreAuthenticatedAuthenticationProvider` をsetしているのか?

前述の通り、`AuthenticationManager` では複数の `AuthenticationProvider` を管理します。

`AuthenticationManager` では、認証時に受け取った `AuthenticationToken` の型に一致する `AuthenticationProvider` がHitするまで、認証処理を問い合わせるという仕様になっています。
 
> AuthenticationManagerの実装クラス内で、[AuthenticationToken に一致する AuthenticationProvider を検知する処理](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/core/src/main/java/org/springframework/security/authentication/ProviderManager.java#L173):
>> ```Java
>>  // ここでTokenの型を取得して
>> 	Class<? extends Authentication> toTest = authentication.getClass(); 
>>	...
>>	int currentPosition = 0;
>>	int size = this.providers.size();
>>  // ここでProviderが受け入れる型と一致するか確認している
>>	for (AuthenticationProvider provider : getProviders()) {
>>		if (!provider.supports(toTest)) {
>>			continue;
>>		}
>>    ...
>> ```


今回 `AbstractPreAuthenticatedProcessingFilter` でリクエストから抽出した認証情報を格納しているのは、

[**PreAuthenticatedAuthenticationToken**](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java)

です。

> [AbstractPreAuthenticatedProcessingFilterにて抽出した認証情報を格納している部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L188):
>> ```
>> PreAuthenticatedAuthenticationToken authenticationRequest = new PreAuthenticatedAuthenticationToken(principal, credentials);
>> ```



この型を持つTokenを元に認証処理を行うのが、`PreAuthenticatedAuthenticationProvider` となっているため、今回 `AuthenticationManager` に当該Providerをsetしています。

### ⑤FilterにAuthenticaitonManagerをsetする

```java
    myPreAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager()); ・・・⑤
```

再掲しますが、`MyPreAuthenticatedProcessingFilter` 内では、認証情報を抽出して、認証に利用するための `PreAuthenticatedAuthenticationToken` を生成します。

このTokenを用いて認証処理を開始するための、`AuthenticationManager`　をsetしています。

`WebSecurityConfigurerAdapter` の `authenticationManager()` メソッドは、[共有の `AuthenticationManager` を返します](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java#L262)。



そのため、先程setした `PreAuthenticatedAuthenticationProvider` がsetされた `AuthenticationManager` が、`myPreAuthenticatedProcessingFilter` 内で有効になります。

###⑥AuthenticationFailureHandlerをFilterにsetする

```java
    myPreAuthenticatedProcessingFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); ・・・⑥
```

`AbstractPreAuthenticatedProcessingFilter` 内で、`PreAuthenticatedAuthenticationToken` を用いて、`AuthenticationManager` にて認証処理を開始する際、認証処理に失敗し、Manager内で認証例外がthrowされた場合、filterに付与された `unsuccessfulAuthentication()` が実行されます。

> [Managerが認証例外をthrowした際にunsuccessfulAuthenticationが実行される部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L195):
>> ```java
>> catch (AuthenticationException ex) {		
>>   unsuccessfulAuthentication(request, response, ex);
>>   if (!this.continueFilterChainOnUnsuccessfulAuthentication) {
>>	   throw ex;
>>   }
>> }
>> ```

`unsuccessfulAuthentication()` では、filter内の `authenticationFailureHandler`　を実行します。

各認証方式のFilterは、デフォルトの `authenticationFailureHandler` として、`SimpleUrlAuthenticationFailureHandler` が指定されています。

> [AbstractAuthenticationProcessingFilter はデフォルトで `SimpleUrlAuthenticationFailureHandler` が指定されている](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java#L134): 
>> ```java
>> private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
>> ```


しかし、`AbstractPreAuthenticatedProcessingFilter` は デフォルトの `authenticationFailureHandler` はnullなので、明示的に `authenticationFailureHandler` を指定する必要があります。



> [`AbstractPreAuthenticatedProcessingFilter` の `authenticationFailureHandler` はデフォルトで null](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L102):
>>```
>> private AuthenticationFailureHandler authenticationFailureHandler = null;
>>```



これにより、APIキーが一致しないケースや、それ以外の認証例外を、自作のハンドラで処理することができます。

ハンドラが発火した際の処理については後述します。


###⑦setCheckForPrincipalChangesにtrueを設定する。

```java
    myPreAuthenticatedProcessingFilter.setCheckForPrincipalChanges(true); ・・・⑦
    myPreAuthenticatedProcessingFilter.setInvalidateSessionOnPrincipalChange(true); ・・・⑦
```

この設定について解説する前に、`SecurityContext` についておさらいする必要があります。

`AuthenticationManager` を通じて認証処理が成功した際、`SecurityContext` には、`AuthenticationToken` が格納されます。



[`SecurityContext` は `HttpSession` に格納](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java#L358)されていますが、HttpSessionは、デフォルトの設定(※5)で、 はステートフルに管理されています。

> (※5)SecurityContextはHttpSession内に保存されます。そのHttpSessionの作成するタイミングを [`SessionCreationPolicy`](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/config/http/SessionCreationPolicy.html) にて操作することができます。[Defaultでは`IF_REQUIRED`](https://github.com/spring-projects/spring-security/blob/d121ab95654300f60e4f9af859f247d8cda66257/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java#L202)が指定されており、必要な場合にのみHttpSessionを作成します。(むやみに作り直さないという表現の方が伝わりやすいかもしれません。)

そのため、Spring Security で管理されたSpring Bootアプリケーションへの接続時、ステートフルな `HttpSession` 内に管理された `SecurityContext` 内に、`AuthenticationToken` が格納されているユーザは認証済みと捉えることができます。



`AbstractPreAuthenticatedProcessingFilter` では、上記の仕組みを利用し、`SecurityContext` 内に、`AuthenticationToken` が既に格納されている場合、認証処理をskipする仕様になっています。

> [AbstractPreAuthenticatedProcessingFilterにて以前認証に成功していた場合にskipする部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L337):
>>```java
>> Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
>>   if (currentUser == null) {
>>	   return true;
>>   }
>>```



その場合、デフォルトの設定では、初回の接続に成功したユーザが、2回目で異なる認証情報を利用していても検知することができません。

```
本来のAPIキー `hello` で疎通した後であれば、

curl --location --request GET 'localhost:9800/hello' \
--header 'Authorization: hello'


不正なAPIキー `he110` でも疎通してしまう。

curl --location --request GET 'localhost:9800/hello' \
--header 'Authorization: he110'
```

**同じ環境にて認証キーを使い分けたいケース**では、認証キーをリクスストの都度確認したいです。

> **同じ環境にて認証キーを使い分けたいケース**: 管理者用アカウントとユーザアカウントで区別する等の、認証キー毎に認可情報を切り替えるケースがあります。

既に`SecurityContext`に `AuthenticationToken` が格納されていても、認証キーをリクエストの都度確認 するための設定が `CheckForPrincipalChanges` です。

`InvalidateSessionOnPrincipalChange` も併用することをおすすめします。(その名の通り、認証キーが異なるケースにて、SessionをInvalidateします。)

> `SessionCreationPolicy` で**同じ環境にて認証キーを使い分けたいケース**に対応する: 下記のように、`WebSecurityConfigurerAdapter` の拡張クラス内、HttpSecurityを引数とする `configure` 内で `SessionCreationPolicy` を以下のように宣言することで、`CheckForPrincipalChanges` を用いずとも、認証キーを都度確認することが可能です。しかし以下の場合、Session内に含まれている `SecurityContext` を再利用できないので、都度認証処理のため、認証情報を保持するデータベースに問合せが発生します。認証情報を使い捨てるような、ワンタイムパスワードを用いた認証等の認証方式で無い限りは、基本的に `CheckForPrincipalChanges` を用いたほうが良さそうです。
>>```java
>> @Override
>> protected void configure(HttpSecurity http) throws Exception {
>>   ...
>> + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
>>   ...
>> }
>>```


###⑧ProviderにAuthenticationUserDetailsServiceをset

```java
    preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new MyAuthenticationUserDetailsService()); ・・・⑧
```


`preAuthenticatedAuthenticationProvider` に、実際の認証処理をsetします。

この `MyAuthenticationUserDetailsService` 内で、受け取ったAPIキーが正しいかどうかを認証します。

`AuthenticationManager` と `AuthenticationProvider` が、1:n の関係だったのに対して、`AuthenticationProvider` と `UserDetailsService` は1:1の関係にあります。

## MyPreAuthenticatedProcessingFilter.java

次に、MyPreAuthenticatedProcessingFilter について解説します。

`AbstractPreAuthenticatedProcessingFilter` を採用している理由は前述の通りです。

```java
package com.example.demo.security.filter;

...

public class MyPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {

  @Override
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).orElse(""); ・・・①
  }

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

}

```

###①リクエストから認証情報を抽出する。

ここでは、Authorization HeaderにAPIキーが含まれているという想定で認証情報を抽出します。

`getPreAuthenticatedPrincipal` では、`Authorization Header` から抽出したAPIキーを返却するようにし、
`getPreAuthenticatedCredentials` では、空文字を返却するようにしています。

これらのメソッドは、`PreAuthenticatedAuthenticationToken` を生成するために利用されています。

> [リクエストから認証情報を抽出する部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L180):
>>```java
>> Object principal = getPreAuthenticatedPrincipal(request);
>> if (principal == null) {
>>     this.logger.debug("No pre-authenticated principal found in request");
>>     return;
>> }
>> this.logger.debug(LogMessage.format("preAuthenticatedPrincipal = %s, trying to authenticate", principal));
>> Object credentials = getPreAuthenticatedCredentials(request);
>> try {
>>   PreAuthenticatedAuthenticationToken authenticationRequest = new PreAuthenticatedAuthenticationToken(principal, credentials);
>> ...


Q. なぜ getPreAuthenticatedPrincipal でAPIキーを返すのか?

A. 前述した `CheckForPrincipalChanges` の設定が有効になっている場合、認証情報の一致確認を行うために利用するのが、`PreAuthenticatedPrincipal` になっています。そのため、`getPreAuthenticatedPrincipal` にAPIキーを配置しています。
> [認証情報の一致確認を行う部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L163) 
>>```java
>> protected boolean principalChanged(HttpServletRequest request, Authentication currentAuthentication) {
>>   Object principal = getPreAuthenticatedPrincipal(request);
>>   if ((principal instanceof String) && currentAuthentication.getName().equals(principal)) {
>>     return false;
>>   }
>> ...
>>```

Q. なぜ `getPreAuthenticatedPrincipal` にて、認証情報が存在しない場合に空文字を返すのか?

`AbstractPreAuthenticatedProcessingFilter`  では、`PreAuthenticatedAuthenticationToken` を生成するという話をしました。



この生成する際、リクエストから認証情報を抽出する際に利用されるのが `getPreAuthenticatedPrincipal` メソッドですが、このメソッドが null を返却した場合、`AbstractPreAuthenticatedProcessingFilter` では、Tokenを作成できないとみなし、以降の認証処理をskipし、次のFilterへ処理が移ります。

> [`getPreAuthenticatedPrincipal` が null を返却した場合、`AbstractPreAuthenticatedProcessingFilter` では認証処理をskipする部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L181): 
>>```java
>> private void doAuthenticate(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
>>   Object principal = getPreAuthenticatedPrincipal(request);
>>   if (principal == null) {
>>     this.logger.debug("No pre-authenticated principal found in request");
>>      return;
>>   }
>> ...
>> ```


###②空文字を返す

Q. なぜ `getPreAuthenticatedCredentials` は空文字を返すのか?

`AbstractPreAuthenticatedProcessingFilter` クラス内のdocでは、credentials は `PreAuthenticatedAuthenticationProvider` にて利用されるため、null を返すべきではない点について言及されています。

> Subclasses must implement the getPreAuthenticatedPrincipal() and getPreAuthenticatedCredentials()methods. Subclasses of this filter are typically used in combination with a PreAuthenticatedAuthenticationProvider, which is used to load additional data for the user. This provider will reject null credentials, so the getPreAuthenticatedCredentials method should not return null for a valid principal.
>> [PreAuthenticatedAuthenticationProvider にて認証処理の実行時、`getPreAuthenticatedCredentials` nullの場合に例外をthrowする部分。](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProvider.java#L89)
>> <br>ここで例外が発生した場合、`UserDetailsService` が実行されない。

上記の通り、null以外の文字を返す必要があります。

今回の認証プロセス上、`Principal` がメインで認証に利用されるため、APIキーを `Credentials` に格納する必要もないため、空文字を返すよう設定しています。

## MyAuthenticationUserDetailsService.java ・ MyUserDetails.java ・ MyAuthenticationFailureHandler

```java
package com.example.demo.security.service;

...

public class MyAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> { ・・・①

  @Override
  public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) { ・・・②

    var myUserDetails = new MyUserDetails(); ・・・③返却用のUserDetailsインターフェースを実装したオブジェクト

    var key = token.getPrincipal().toString();  ・・・④APIキーを抽出

    /**
     * Check API Key
     */
    if (!(key.equals("hello") || key.equals("world"))) { ・・・⑤一致確認: 通常ここでDBアクセス等の処理を追加する
      throw new UsernameNotFoundException("Invalid key.");
    }

    myUserDetails.setKey(key); ・・・⑥

    return myUserDetails;
  }
}
```

###①AuthenticationUserDetailsServiceについて
`AuthenticationUserDetailsService` は、Provider経由で受け取った`AuthenticationToken`を認証します。

ここでは、`AuthenticationToken` から認証情報を抽出し、その認証情報が正しいかどうかを検証するために、Databaseへの問合せ等を通じて、認証情報の照合を行います。

照合が成功した場合は `UserDetails` オブジェクトに認証情報をsetして返却します。

Genericsとして `PreAuthenticatedAuthenticationToken` が指定されていますが、これは前述の、受け取ったTokenの型から、適切なProviderを推測するというManager内の処理に起因します。

Providerでは受け取るTokenの型が決まっているため、ここでもTokenの型を指定する必要があります。



Providerに返却した`UserDetails`Objectを元に、Providerは再度 `PreAuthenticatedAuthenticationToken` を生成します。

>[Provider内でUserDetailsを元に再度Tokenを生成する部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProvider.java#L99):
>>```java
>> UserDetails userDetails = this.preAuthenticatedUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationToken) authentication);
>> this.userDetailsChecker.check(userDetails);
>> PreAuthenticatedAuthenticationToken result = new PreAuthenticatedAuthenticationToken(
>>   userDetails,
>>   authentication.getCredentials(),
>>   userDetails.getAuthorities()
>> );
>> result.setDetails(authentication.getDetails());
>> ```

何故 `AuthenticationUserDetailsService` で認証に成功した `PreAuthenticatedAuthenticationToken` をそのまま返却せず、`UserDetails`Objectを返却後、ProviderにてAuthenticationTokenを再生成するのか については後述します。



###②loadUserDetailsメソッドについて

loadUserDetailsメソッドは、Provider内で認証を開始するために呼び出されます。

概要として、`AuthenticationToken`を受け取り、`UserDetails`オブジェクトを返すという役割を担っています。

このメソッドは `UserDetails` 以外は返却しない仕様になっており、仮に認証を失敗扱いとしたい場合は、`UsernameNotFoundException` をthrowすることで、呼び出し元のProviderに認証が失敗したことを伝える仕様になっています。

###⑥なぜAPIキーをsetするのか?

setされたAPIキーは、`MyUserDetails` クラス内の、`getUsername` メソッドから返却されるよう実装されています。

```java
package com.example.demo.security.model;

...

public class MyUserDetail implements UserDetails {

  private String key;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
  }

  @Override
  public String getPassword() {
    return null;
  }

  @Override
  public String getUsername() { ・・・ココ
    return key;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}

```

前述しましたが、`AuthenticationUserDetailsService`からProviderに返却されたUserDetailsは、Provider内で再度Tokenを生成する際に利用されます。

>```java
> UserDetails userDetails = this.preAuthenticatedUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationToken) authentication);
> this.userDetailsChecker.check(userDetails);
> PreAuthenticatedAuthenticationToken result = new PreAuthenticatedAuthenticationToken(
>   userDetails,
>   authentication.getCredentials(),
>   userDetails.getAuthorities()
> );
> result.setDetails(authentication.getDetails());
> ```

この再生成時に生成される `PreAuthenticatedAuthenticationToken` の `getName` メソッドは、基本的に `UserDetails` オブジェクトの `getUsername` メソッドを元に値を返します。

> [getUsernameがgetName内でcallされている部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java#L72): 
>>```java
>> public String getName() {
>>   ...
>>   return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
>>   ...
>>```

この `PreAuthenticatedAuthenticationToken` の `getName` メソッドは、前述の、`CheckForPrincipalChanges` を通じた都度認証時に利用されます。

> [以前の認証情報を再利用できるかどうか確認する部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L164):
>>```java
>> protected boolean principalChanged(HttpServletRequest request, Authentication currentAuthentication) {
>>   Object principal = getPreAuthenticatedPrincipal(request);
>>   if ((principal instanceof String) && currentAuthentication.getName().equals(principal)) {
>>     return false;
>>   }




抽出した値を `getUsername` で返却するよう `UserDetails` にsetすることで、
次回以降の接続時、都度APIキーを確認する処理が正常に実行されます。

もしここで `getUsername` でAPIキーを返すよう指定しない場合、次回の認証で用いられる認証情報が空になったまま、`SecurityContext` に `PreAuthenticatedAuthenticationToken` が登録されてしまいます。

そうなると、

前回の認証情報 : 空
今回の認証情報 : 正しいAPIキー

が比較され、都度前回と異なるAPIキーとして認証処理が実行されてしまいます。

###UserDetailsの他のメソッドについて

**getPassword**

今回のケースでは、このメソッドが参照されることがないので、nullを返却しています。
ここでは最低限の実装を考慮しているため、nullを返却していますが、自身の認証方式で必要とあらば、任意の値を入れても構いません。

**is◯◯◯◯**

APIキーは認証情報が一致しないケース以外にも、認証を失敗として扱いたいケースがいくつかあります。

例えば、認証情報の有効期限が切れているときや、事業者側の事情で強制的に該当ユーザを失敗させたいときなどがあります。



このような、認証情報以外の事由で認証を拒否する条件がある場合に利用すると良いのが `is` を接頭辞に持つメソッドです。

例えば、認証情報の有効期限が切れている際に認証を失敗としたいときは、以下のように記載します。


```java

public class MyAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> { 

  @Override
  public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) { 

    ...

    myUserDetail.setCreatedAt(createdAt) ・・・DBから取得した「認証情報の生成された時刻」をuserDetailsにset

    ...

```


```java
  
  Date createdAt; ・・・ここにsetされる
 
  public boolean isAccountNonExpired() {

    if (createdAt から2年以上立っている){
      return false
    } else {
      return true;
    }
  
  }
```

これらの is◯◯◯◯ メソッドは、Provider上で、再度 AuthenticationTokenを発行する前に、一度検閲されます。

> [is◯◯◯◯ を実行し、認証失敗としたいケースに該当するか確認する部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationProvider.java#L98)
>> ```java
>> this.userDetailsChecker.check(userDetails);
>> ```


もし認証失敗のケースを確認した場合は、それぞれに適した例外をthrowします。

> [認証に失敗したケースごとに例外をthrowする部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/core/src/main/java/org/springframework/security/authentication/AccountStatusUserDetailsChecker.java#L40)

```java
	public void check(UserDetails user) {
		if (!user.isAccountNonLocked()) {
			this.logger.debug("Failed to authenticate since user account is locked");
			throw new LockedException(
					this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));
		}
		if (!user.isEnabled()) {
			this.logger.debug("Failed to authenticate since user account is disabled");
			throw new DisabledException(
					this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));
		}
		if (!user.isAccountNonExpired()) {
			this.logger.debug("Failed to authenticate since user account is expired");
			throw new AccountExpiredException(
					this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired"));
		}
		if (!user.isCredentialsNonExpired()) {
			this.logger.debug("Failed to authenticate since user account credentials have expired");
			throw new CredentialsExpiredException(this.messages
					.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
		}
	}
```

###MyAuthenticationFailureHandler
MyAuthenticationFailureHandlerでは、これらの例外毎に、遷移先を変更することも可能です。

```java
package com.example.demo.security.exception.handler.authentication;

...

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {


    if (exception instanceof AccountExpiredException) {
        // 有効期限を延長するページへリダイレクト 
        response.sendRedirect("https://~");
        return;
    }

    /**
     * Redirect API Key Reset Link
     */
    response.sendRedirect("https://www.google.com");
  }
}
```

AuthenticationFailureHandler は、Filter内での認証失敗時にcallされます。
仕組みは単純で、認証処理中に起きた認証に関する例外 `AuthenticationException` をそのまま受け取ります。

> [認証失敗時にcallする部分](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java#L194)
>>```java
>> catch (AuthenticationException ex) {
>>   unsuccessfulAuthentication(request, response, ex);
>>   if (!this.continueFilterChainOnUnsuccessfulAuthentication) {
>>     throw ex;
>>   }
>> }
>>```



##まとめ

ここまで、APIキーによる認証方式を最低限で実装するための解説をしてきました。

実際の現場では、認証のみを提供することは少ないかと思いますが、以降の理解に役に立つと思うので、敢えて独立したパートとして記載してみました。

#4.APIキーによる認証・認可の実装例

ここからは、前半で解説した認証方式に、認可を実装したいと思います。

##ディレクトリ構成
```
demo
├── DemoApplication.java
├── controller
│   └── HelloWorldController.java
└── security
    ├── config
    │   └── MyWebSecurityConfigurer.java ・・・ update
    ├── exception
    │   └── handler
    │       ├── authentication
    │       │   ├── MyAuthenticationEntryPoint.java ・・・ new
    │       │   └── MyAuthenticationFailureHandler.java 
    │       └── authorization
    │           └── MyAccessDeniedHandler.java ・・・ new
    ├── filter
    │   └── MyPreAuthenticatedProcessingFilter.java 
    ├── model
    │   └── MyUserDetails.java ・・・ update
    └── service
        └── MyAuthenticationUserDetailsService.java ・・・ update
```

##MyWebSecurityConfigurer.java

```java
package com.example.demo.security.config;

...

@Configuration
@EnableWebSecurity
class MyWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
　　
  ...

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

    http.addFilter(getMyPreAuthenticatedProcessingFilter());

+   http.authorizeRequests()
+     .antMatchers("/world").hasAuthority("WORLD")
+     .anyRequest().authenticated();

+   http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());

+   http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
  }

  public AbstractPreAuthenticatedProcessingFilter getMyPreAuthenticatedProcessingFilter() throws Exception {
    var myPreAuthenticatedProcessingFilter = new MyPreAuthenticatedProcessingFilter();
    myPreAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager());
-   myPreAuthenticatedProcessingFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
    myPreAuthenticatedProcessingFilter.setCheckForPrincipalChanges(true);
    myPreAuthenticatedProcessingFilter.setInvalidateSessionOnPrincipalChange(true);
    return myPreAuthenticatedProcessingFilter;
    }


  ...

}
```

##MyUserDetails.java

```java
package com.example.demo.security.model;

...

@Data
public class MyUserDetails implements UserDetails {

　...

+ private List<GrantedAuthority> authorities;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
+   return authorities;
  }
 
  ...
  
}

```

## MyAuthenticationUserDetailsService


```java
package com.example.demo.security.service;

...

public class MyAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

  @Override
  public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {

    ...
+   /**
+    * Add Authority
+    */
+   if (key.equals("world")) {
+     List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
+     authorities.add(new SimpleGrantedAuthority("WORLD"));
+     myUserDetails.setAuthorities(authorities);
+   }

    return myUserDetails;
  }
}

```

##MyAccessDeniedHandler.java

```java
package com.example.demo.security.exception.handler.authorization;

...

public class MyAccessDeniedHandler implements AccessDeniedHandler {
  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
  }
}
```

##MyAuthenticationEntryPoint.java

```java
package com.example.demo.security.exception.handler.authentication;

...

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        /**
         * API Key Reset Link
         */
        response.sendRedirect("https://yahoo.co.jp");
    }
}
```

#5.認可の実装例の解説

ここでは、実際に認可処理がどのように行われているかは解説せずに、

APIキーによる認証・認可のエラーハンドリングについて解説します。

※ 実際の認可処理は高度にカプセル化されており、カスタムするケースが稀なことや、認可処理に関する記事は既に多く公開されていることから、認可処理がどのように構築されているかをここで解説しません。

##MyWebSecurityConfigurer.java

```java
package com.example.demo.security.config;

...
@Configuration
@EnableWebSecurity
class MyWebSecurityConfigurer extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilter(getMyPreAuthenticatedProcessingFilter());

+   http.authorizeRequests() ・・・①
+     .antMatchers("/world").hasAuthority("WORLD") 
+     .anyRequest().authenticated(); 

+   http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint()); ・・・②

+   http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler()); ・・・②
  }

  public AbstractPreAuthenticatedProcessingFilter getMyPreAuthenticatedProcessingFilter() throws Exception {
    var myPreAuthenticatedProcessingFilter = new MyPreAuthenticatedProcessingFilter();
    myPreAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager());
-   myPreAuthenticatedProcessingFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); ・・・③
    myPreAuthenticatedProcessingFilter.setCheckForPrincipalChanges(true);
    myPreAuthenticatedProcessingFilter.setInvalidateSessionOnPrincipalChange(true);
    return myPreAuthenticatedProcessingFilter;
    }

  ...

}
```

###① `authorizeRequests` について

`authorizeRequests` を利用し、URLに基づく認可ルールを構成することができます。

今回は2つのルールを追加しています。

```java
.antMatchers("/world").hasAuthority("WORLD") 
```

を通じて、`/world` エンドポイントについては、`WORLD` Authorityを持つユーザのみを認可します。

また、

```java
.anyRequest().authenticated(); 
```

を通じて、いづれのリクエストについても認証を実行します。

###② `MyAccessDeniedHandler`・ `MyAuthenticationEntryPoint` について

ここで、前半と後半のSecurity FIlterを比較してみましょう。

_前半: 認証処理のみ_

```java
Will secure any request with [
  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@642505c7, 
  org.springframework.security.web.context.SecurityContextPersistenceFilter@40e60ece, 
  org.springframework.security.web.header.HeaderWriterFilter@5fac521d, 
  org.springframework.security.web.csrf.CsrfFilter@782bf610, 
  org.springframework.security.web.authentication.logout.LogoutFilter@476e8796, 
  com.example.demo.security.filter.MyPreAuthenticatedProcessingFilter@XXXXXXXX,
  org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3a230001, 
  org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4d654825, 
  org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4339e0de, 
  org.springframework.security.web.session.SessionManagementFilter@706eab5d, 
  org.springframework.security.web.access.ExceptionTranslationFilter@1846579f
]
```

_後半: 認証・認可処理_

```java
 Will secure any request with [
   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@149c3204, 
   org.springframework.security.web.context.SecurityContextPersistenceFilter@6ab7ce48, 
   org.springframework.security.web.header.HeaderWriterFilter@148c7c4b, 
   org.springframework.security.web.csrf.CsrfFilter@6ffa56fa, 
   org.springframework.security.web.authentication.logout.LogoutFilter@7a1f45ed, 
   com.example.demo.security.filter.MyPreAuthenticatedProcessingFilter@64f16277, 
   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@e322ec9, 
   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4d27d9d, 
   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@497aec8c, 
   org.springframework.security.web.session.SessionManagementFilter@459cfcca, 
   org.springframework.security.web.access.ExceptionTranslationFilter@51a8313b, 
+  org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2de50ee4
]
```

末尾に `FilterSecurityInterceptor` というFilterが追加されています。
このFilterが、認可処理を行うFilterになっています。

> tips: このFilterだけ、末尾がFilterではなく、Interceptor となっていますね。[JavaDoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/HandlerInterceptor.html) によると、複雑な前処理を行う場合は `Interceptor` を利用するべきだと記載してあります。確かに、この `FilterSecurityInterceptor` では認可処理という複雑な処理を行っています。しかし、`FilterSecurityInterceptor`が[実装しているインターフェースはFilter](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/web/src/main/java/org/springframework/security/web/access/intercept/FilterSecurityInterceptor.java#L45)であり、JavaDoc内に記載の `HandlerInterceptor` インターフェースとは異なります。ここでは、複雑なことをしているFilterだから、`Interceptor` と呼んでいるのだなという程度の認識に留めておいて結構です。
>> As a basic guideline, fine-grained handler-related preprocessing tasks are candidates for HandlerInterceptor implementations

このFilterは、`authorizeRequests` 句等の認可にまつわる処理を記載した場合、Security Filterの末尾に追加されます。

> [`createFilterSecurityInterceptor` メソッド](https://github.com/spring-projects/spring-security/blob/d121ab95654300f60e4f9af859f247d8cda66257/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java#L630)



これから解説する `MyAccessDeniedHandler` は、認可処理で起きたエラーを ハンドリングするためのクラスですが、 `FilterSecurityInterceptor` では利用されず、`ExceptionTranslationFilter` にて利用されます。

後半では、この `ExceptionTranslationFilter` を中心に巻き起こる、認可処理のエラーハンドリングについて解説します。

---

###エラーハンドリングについて

では早速エラーハンドリングについて解説を始めていきます。

エラーハンドリングは大きく2種類に別れます。

1種類目は**認証に失敗したケース**です。

```
1. APIキーが指定されていないケース
2. APIキーが一致しないケース (今回だと、helloとworld以外のAPIキーが指定されているケース)
3. APIキーは一致しているが、その他の理由で認証が失敗するケース(上述のcheckメソッドを通じて有効期限切れを検知するケース)
```

2種類目は**認証には成功したが、認可で失敗するケース**です。

今回のケースで言えば、`/world` エンドポイントに接続するには、`WORLD` 権限が付与されるAPIキー `world` を利用する必要があります。

しかし、その `/world` エンドポイントに対して、`WORLD` 権限が付与されないAPIキー `hello` を用いて接続しようとすると、APIキー自体は有効であるものの、`WORLD` 権限を持っていないため、認可に失敗し接続できません。

それでは、それぞれのケースについて、どのようにSpring Securityでエラーハンドリングが行われているかを解説します。

#### ケース1: 認証に失敗したケース

前半解説した、認証のみを実装しているケースでは、認証に失敗すると、まず始めに `MyPreAuthenticatedProcessingFilter` にsetされている `MyAuthenticationFailureHandler` 内の `onAuthenticationFailure` メソッドが発火していました。

```java
package com.example.demo.security.exception.handler.authentication;

...

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    /**
     * Redirect API Key Reset Link
     */
    response.sendRedirect("https://www.google.com"); ・・・ココが発火していた。
  }
}
```

> `onAuthenticationFailure` メソッド: このメソッドは、Applicationを利用したユーザに対して、認証情報を適切に発行するよう誘導するような使い方をします。例えば、APIキーが違うようなら、APIキーを発行時のメールアドレスを入力させるようなページに遷移させたり、APIキーの有効期限が切れているようなら、APIキーを更新するページへ遷移させたりします。



しかし認証・認可のケースでは、`MyAuthenticationFailureHandler` はsetされていません。
この理由については後述します。

```java
  public AbstractPreAuthenticatedProcessingFilter getMyPreAuthenticatedProcessingFilter() throws Exception {
    var myPreAuthenticatedProcessingFilter = new MyPreAuthenticatedProcessingFilter();
    myPreAuthenticatedProcessingFilter.setAuthenticationManager(authenticationManager());
-   myPreAuthenticatedProcessingFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); ・・・③
    myPreAuthenticatedProcessingFilter.setCheckForPrincipalChanges(true);
    myPreAuthenticatedProcessingFilter.setInvalidateSessionOnPrincipalChange(true);
    return myPreAuthenticatedProcessingFilter;
    }
```

認証に失敗した際、`MyAuthenticationFailureHandler` がsetされていないため、デフォルトで `AuthenticationFailureHandler` がnullになっている `AbstractPreAuthenticatedProcessingFilter` では、認証失敗を検知しないまま、後続のFilterの処理を実行します。

---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する

```

通常 `AuthenticationFilter` は、認証に成功した場合、先述の `SecurityContext` 内に、認証情報 `AuhenticationToken` をsetします。

> 公式ドキュメントより引用: [Authenticationは、現在認証されているユーザーを表します。現在の Authentication は SecurityContext から取得できます。](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#servlet-authentication-authentication)

しかし今回のような認証に失敗したケースでは、`AuthenticationFilter` から `SecurityContext` 内に `AuhenticationToken` がsetされません。

`SecurityContext` 内に `AuhenticationToken` がsetされていないまま、後続のFilter処理が進んでいくと(※6)、
いづれ `AnonymousAuthenticationFilter` の処理に移ります。

```java
   ...
   com.example.demo.security.filter.MyPreAuthenticatedProcessingFilter@64f16277, 
   
   ...

   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@497aec8c, 

   ...
```

`AnonymousAuthenticationFilter` にて、`SecurityContext` 内に なんらかの `AuhenticationToken` がsetされていないことを確認したとき、`SecurityContext` 内に `AnonymousAuhenticationToken` を設置します。

> 公式ドキュメントより引用: **AnonymousAuthenticationFilter** - [通常の認証メカニズムの後にチェーンされ、既存の Authentication が保持されていない場合、AnonymousAuthenticationToken を SecurityContextHolder に自動的に追加します。](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#anonymous-config)

---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
```

`AnonymousAuthenticationFilter` にて `SecurityContext` に  `AnonymousAuhenticationToken` がsetされたあとは、以下の順でFilterが実行されます。

```
   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@497aec8c, 
   org.springframework.security.web.session.c@459cfcca, 
   org.springframework.security.web.access.ExceptionTranslationFilter@51a8313b, 
+  org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2de50ee4
```

> tips: [SessionManagementFilter](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/session/SessionManagementFilter.html) では、sessionを改ざんした不正なリクエストをブロックしたり、複数ログインに対する制御を行います。今回は最低限の認証・認可を実装するというコンセプトなので、このFilterについての解説は省略させていただきます。

一見、`AnonymousAuthenticationFilter` のあとに、`ExceptionTranslationFilter` が来ているので、先に `ExceptionTranslationFilter` の処理が発火するかのように見えます。

しかし、ここでは、`ExceptionTranslationFilter` は後続の`FilterSecurityInterceptor` を実行するのみで、前処理は何もしません。

```java
  ...
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
      // ExceptionTranslationFilterでは前処理が記載されておらず
      // FilterSecurityInterceptorを実行するのみ
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
    ... 
```

`ExceptionTranslationFilter` では、try句内で、`FilterSecurityInterceptor` を実行し、その例外処理のみを担っています。

---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
```

**FilterSecurityInterceptor**

ということで、try句内で実行される `FilterSecurityInterceptor` 内の処理を確認します。

現段階で `SecurityContext` には `AnonymousAuhenticationToken` が設定されています。

無論、`AnonymousAuhenticationToken` には何の権限も保持していないため、`FilterSecurityInterceptor` は認可に失敗したことを示す `AccessDeniedException` をthrowします。
> tips: 認可処理の流れは割愛しますが、気になる人は認可処理を呼んでみてください: [デフォルトのvoterを設定している箇所](https://github.com/spring-projects/spring-security/blob/7a233c41f0d76c4d44d62689a15e9f9b7e098ce0/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java#L244)と、その[Voterを実行しているクラス](https://github.com/spring-projects/spring-security/blob/006b9b960797d279b31cf8c8d16f1549c5632b2c/core/src/main/java/org/springframework/security/access/vote/AffirmativeBased.java)。これらが `FilterSecurityInterceptor` 内の `beforeInvocation` メソッドを通じてcallされている。


---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
```


ステップ3で、`ExceptionTranslationFilter` のtry句内で、`FilterSecurityInterceptor` が実行されていることを解説しました。

そのため、さきほどthrowされた `AccessDeniedException` は、 `ExceptionTranslationFilter` にてcatchされます。

`AccessDeniedException` をcatchした際の処理は下記です。

```java
    ...
		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
      // StackTraceに書き込まれた例外から、AuthenticationExceptionを探す
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
      
			if (securityException == null) {
        // AuthenticationExceptionがStackTraceから見つからない場合、
        // AccessDeniedExceptionを探す
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			if (securityException == null) {
				rethrow(ex);
			}
			if (response.isCommitted()) {
        // もしresponseがコミットされている場合は、
        // ServletExceptionをthrowし、エラーハンドリングを終了する
				throw new ServletException("Unable to handle the Spring Security Exception "
						+ "because the response is already committed.", ex);
			}
			handleSpringSecurityException(request, response, chain, securityException);
		}
    ...

```
`ExceptionTranslationFilter` では、`AccessDeniedException` をStackTraceから取得します。
最終的に、`ExceptionTranslationFilter` では `handleSpringSecurityException` を実行します。

```java
	private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, RuntimeException exception) throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
		}
	}
```
---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
5. ExceptionTranslationFilterにてhandleSpringSecurityExceptionが実行される
```

`handleSpringSecurityException` では、`AccessDeniedException` 型を検知し、`handleAccessDeniedException` が実行されます。

```java
	private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
		if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
						authentication), exception);
			}
			sendStartAuthentication(request, response, chain,
					new InsufficientAuthenticationException(
							this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
									"Full authentication is required to access this resource")));
		} else { ...

```

---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
5. ExceptionTranslationFilterにてhandleSpringSecurityExceptionが実行される
6. handleSpringSecurityExceptionにてhandleAccessDeniedExceptionが実行される
```

ここでは、`SecurityContext` に配置されている　`AuthenticationToken` を確認しています。



そして、もし `AnonymousAuthenticationToken`　が配置されていた場合は、`sendStartAuthentication` メソッドを通じ、`InsufficientAuthenticationException` によって、`MyAuthenticationEntryPoint` を発火させます。



```java
	protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// SEC-112: Clear the SecurityContextHolder's Authentication, as the
		// existing Authentication is no longer considered valid
		SecurityContextHolder.getContext().setAuthentication(null);
		this.requestCache.saveRequest(request, response);
		this.authenticationEntryPoint.commence(request, response, reason);
	}
```

---

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
5. ExceptionTranslationFilterにてhandleSpringSecurityExceptionが実行される
6. handleSpringSecurityExceptionにてhandleAccessDeniedExceptionが実行される
7. SecurityContextのAnonymousAuthenticationTokenを検知し、sendStartAuthenticationが実行される
```


この後、`MyAuthenticationEntryPoint` 内の 

```java
response.sendRedirect("https://yahoo.co.jp");
```

を通じて、クライアントを任意のサイトにリダイレクトします。

これまでの処理をまとめると、認証に失敗した場合、処理は以下のように進行します。

```
現在の流れ
1. 認証に失敗後、失敗を検知せずに後続のFilterを実行する
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
5. ExceptionTranslationFilter にて handleSpringSecurityException が実行される
6. handleSpringSecurityException にて handleAccessDeniedException が実行される
7. SecurityContext の AnonymousAuthenticationToken を検知し、sendStartAuthentication が実行される
8. sendStartAuthentication にて MyAuthenticationEntryPointが実行される
```

このエラーハンドリングでは、

**`AnonymousAuthenticationToken` が `SecurityContext` に配置されている場合、認証に失敗していると見做す**

という前提の元、処理が進んでいることを理解しておくと良いでしょう。

####ケース2: 認証には成功したが、認可で失敗するケース

ケース1の流れを押さえていれば、ケース2は単純です。

認証に成功しているということは、

**`AnonymousAuthenticationToken` が `SecurityContext` に配置されていない**

ということです。

今回のケースでは、認証に成功しているため、`PreAuthenticatedAuthenticationToken` が `SecurityContext` に配置されています。



認証に成功したが、認可に失敗するケースでは、以下のように処理を実行します。

```
現在の流れ
1. 認証に成功後、SecurityContext に PreAuthenticatedAuthenticationToken を配置し、後続のFilterを実行する
2. SecurityContext に Token が配置済み(認証済み) のため、AnonymousAuhenticationToken を配置せず後続のFilterを実行する
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
5. ExceptionTranslationFilter にて handleSpringSecurityException が実行される
6. handleSpringSecurityException にて handleAccessDeniedException が実行される
7. handleAccessDeniedException にて MyAccessDeniedHandler が実行される
```

違いは、

1. 認証に成功しているため、`AnonymousAuthenticationFilter` にて `SecurityContext` に  `AnonymousAuhenticationToken` がsetされない

2. `ExceptionTranslationFilter` 内の `handleAccessDeniedException` にて、`AnonymousAuhenticationToken` の制御を受けずに `MyAccessDeniedHandler` を実行する

という2点です。

###③なぜ事前認証ケースにおける認証・認可では、FailureHandlerをFilterにsetしないのか?

結論から言うと、仕様上 `AuthenticationEntryPoint` にて認証失敗エラーをハンドリングするようになっているからです。



仮にsetされていた状態で、認証・認可の処理が進んでいたとします。

誤った認証情報を用いて、アプリケーションに接続を試みたとき、`FailureHandler` の `onAuthenticationFailure` メソッドが発火します。

前述の通り、認証に失敗したユーザを、再度認証するためのページに誘導する役割を担うべきとされる `FailureHandler` では、以下の `sendRedirect` 句が用いられます。この時点で、responseはcommitされます(※6)。

```
response.sendRedirect("https://www.google.com");
```

> (※6) **ServletResponseのcommitとは?**: 
>> sentRedirectが実行された時点で、ユーザは特定のURLに遷移するわけではありません。<br>この `sendRedirect` メソッドが実行されると、response こと `ServletResponse` は `commit済み` というステータスになるだけで、後続の処理が中断されるわけではありません。<br>無論Filterの処理は継続し、最終的に全てのFilterの前処理・後処理を終えたあと、`commit済み` ステータスに切り替わったresponseがクライアントに返送されます。<br>`sendRedirect` メソッド以外にも、`ServletResponse` をcommit済みとするメソッドはいくつかあります。また、commit済みになって以降、その `ServletResponse` を更新することはできません。(※ 個人的なハマりポイントだったので、少し突っ込んで解説しています。)

このまま、前述で解説したように、処理を進めていきます。

そして、5つ目の処理を確認してみます。



```
現在の流れ
1. 認証に失敗したため response.sendRedirect が発火され、response がcommit済みとなる。
2. 空の SecurityContext に AnonymousAuhenticationToken を配置
3. ExceptionTranslationFilter 内のtry句で FilterSecurityInterceptorを実行
4. FilterSecurityInterceptor が AccessDeniedException をthrowする
5. ExceptionTranslationFilter にて ・・・

```

1つ目の処理にて、既に `response.sendRedirect` が実行され、responseがコミット済みとなっています。

```java
    ...
		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
      // StackTraceに書き込まれた例外から、AuthenticationExceptionを探す
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
      
			if (securityException == null) {
        // AuthenticationExceptionがStackTraceから見つからない場合、
        // AccessDeniedExceptionを探す
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			if (securityException == null) {
				rethrow(ex);
			}
			if (response.isCommitted()) {
        // もしresponseがコミットされている場合は、
        // ServletExceptionをthrowし、エラーハンドリングを終了する
				throw new ServletException("Unable to handle the Spring Security Exception "
						+ "because the response is already committed.", ex);
			}
			handleSpringSecurityException(request, response, chain, securityException);
		}
    ...

```

そのため、`ExceptionTranslationFilter` のcatch句で、例外をハンドリングしようと思ったとき、

`response.isCommitted()` の条件がtrueとなってしまうため、
`handleSpringSecurityException` を実行する前に、`ServletException` がthrowされてしまいます。

> 実際のエラー: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause

仮にこのif文がなかったとしても、`response.sendRedirect` にてcommit済みのresponseを、 `MyAccessDeniedHandler` や `MyAuthenticationEntryPoint` にて更にcommitしてしまうので、以下のようなエラーが発生するでしょう。

> `java.lang.IllegalStateException: ~ Response already committed. `

このように、認証・認可処理にて、`AuthenticationFailureHandler` 本来の使い方をしようと思うと、`ExceptionTranslationFilter` の2重commitを防ぐためのLogicによって、内部エラーが発生してしまいます。

この仕様を見るに、認証・認可処理において、認証エラーについては、`AuthenticationEntryPoint` を用いるのが正しいと言えそうです。

> InsufficientAuthenticationException しか検出できない件について:<br>`AuthenticationEntryPoint`を用いて認証エラーをハンドリングする際、全ての認証エラーは `InsufficientAuthenticationException` でthrowされます。つまり、`AuthenticationFailureHandler`のように、発生した例外別に、遷移先を用意することができません。これは、認証エラーを `AnonymousAuhenticationToken` の有無で判別するというSpring Securityの仕様上の問題です。現在この問題についてissueは上がっているものの、優先順位は低いものとなっています。
>> 認証例外が `InsufficientAuthenticationException` によって上塗りされてしまう件について :https://github.com/spring-projects/spring-security/issues/4630



#最後に

さて、ここまでに、事前認証ケースにおける、APIキーによる認証方式の実装例を解説してきました。

複数の実装方法がある中で、それぞれの方法を比較検討し、コードに落とし込むことは困難です。

特にAPIキーの認証方式(特にJWTを用いた認証方式等)について検索すると、GenericFilterBean や OncePerRequestFilter を拡張し、addFilterBefore句でFilterを追加したり、SecurityContextの管理を直書きするケースが散見されますが、よほど特別なケースで無い限りは、Spring Securityから提供されている pre-auth パッケージを正しく利用するほうが、考えることは少なくて済むと思っています。

この実装例から、それぞれに最適な認証方式を確立できれば嬉しいなと思っています。