Spring Boot + Spring Security で BASIC 認証を実現する

## 概要
- Spring Boot と Spring Security を使って BASIC 認証を実現する
- 今回の環境: Java 11 + Spring Boot 2.2.1 + Spring Security 5.2.1 + Gradle 6.0.1

## BASIC 認証とは

[Basic認証 \- Wikipedia](https://ja.wikipedia.org/wiki/Basic%E8%AA%8D%E8%A8%BC)

> Basic認証では、ユーザ名とパスワードの組みをコロン ":" でつなぎ、Base64でエンコードして送信する。このため、盗聴や改竄が簡単であるという欠点を持つが、ほぼ全てのWebサーバおよびブラウザで対応しているため、広く使われている。

[HTTP 認証 \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Authentication)

> ユーザーIDとパスワードはネットワークを介してクリアテキスト (base64 でエンコードされていますが、base64 は可逆エンコードです) として渡されるため、Basic 認証方式は安全ではありません。

[Authorization \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Authorization)

> Base64 エンコードは暗号化でもハッシュでもありません。この方法の安全性はクリアテキストで認証情報を送るのと同等です (Base64 は可逆エンコーディングです)。 Basic 認証は HTTPS との組み合わせで使用することをお勧めします。

## Spring Security の導入 (build.gradle)

build.gradle の dependencies にて spring-boot-starter-security を導入する。

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

今回は [Spring Initializr](https://start.spring.io/) にて以下を指定して build.gradle を作成した。

- Project: Gradle Project
- Language: Java
- Spring Boot: 2.2.1
- Packaging: Jar
- Java: 11
- Dependencies: Spring Security, Spring Web, Thymeleaf

生成された build.gradle ファイルの中身は以下のようになっている。

```build.gradle
plugins {
  id 'org.springframework.boot' version '2.2.1.RELEASE'
  id 'io.spring.dependency-management' version '1.0.8.RELEASE'
  id 'java'
}

group = 'com.example'
version = '0.0.1'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-security'
  implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
  testImplementation 'org.springframework.security:spring-security-test'
}

test {
  useJUnitPlatform()
}
```

## BASIC 認証を実現するコード

BASIC 認証を実現するためにはクラスが2つがあれば良い。
ひとつは設定情報を定義するための WebSecurityConfigurer インターフェースの実装クラス。
もうひとつは BASIC 認証を処理するための AuthenticationProvider インターフェースの実装クラス。

Spring Boot として動作させるためにはコントローラークラス等が必要だが、ここでは記載しない。

### WebSecurityConfigurer インターフェースの実装クラスを用意する

どのような URL にアクセスしたときに認証を必要とするのか、どのような認証方式を使用するのか等の設定をするためのクラスを用意する。

```java
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

/**
 * WebSecurity などの設定をするためのクラス。
 * WebSecurityConfigurer インターフェースの実装クラスであり、
 * 抽象クラスである WebSecurityConfigurerAdapter を継承している。
 */
@Configuration
@EnableWebSecurity // Spring Security を有効化
public class DemoBasicAuthConfiguration extends WebSecurityConfigurerAdapter {

  /**
   * WebSecurity の設定をする。
   * 特定のリクエストを無視する設定など、主に全体的なセキュリティ設定を行う。
   * Spring Boot 起動時に呼び出される。
   *
   * @param web 全体的なセキュリティ設定
   * @throws Exception エラーが発生した場合
   */
  @Override
  public void configure(WebSecurity web) throws Exception {

    System.out.println("DemoBasicAuthConfiguration#configure(WebSecurity)");

    // css や js ファイルに認証を必要としない設定を記述
    web.ignoring().antMatchers("/secret-garden/css/**", "/secret-garden/js/**");
  }

  /**
   * HttpSecurity の設定をする。
   * 主に特定の HTTP リクエストに対するセキュリティ設定を実施する。
   * 通常は super で WebSecurityConfigurerAdapter#configure をコールしてはいけない。
   * デフォルトでは http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); で構成されている。
   * Spring Boot 起動時に呼び出される。
   *
   * @param http 特定の HTTP リクエストに対するセキュリティ設定
   * @throws Exception エラーが発生した場合
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {

    System.out.println("DemoBasicAuthConfiguration#configure(HttpSecurity)");

    // CSRF 対策機能を無効化
    http.csrf().disable();

    // 認証が必要な URL を指定
    http.antMatcher("/secret-garden/**");

    // 指定した URL を対象とした認証を有効化
    http.authorizeRequests().anyRequest().authenticated();

    // BASIC 認証を有効化
    // 認証領域 (authentication realm) の名称も指定
    http.httpBasic().realmName("Your secret area.");

    // BASIC 認証の情報は毎回送信されるためセッション管理は不要
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  }

  /**
   * AuthenticationManagerBuilder の設定をする。
   * 認証の実装である AuthenticationProvider を指定する。
   * Spring Boot 起動時に呼び出される。
   *
   * @param auth AuthenticationManager を生成するオブジェクト
   * @throws Exception エラーが発生した場合
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    System.out.println("DemoBasicAuthConfiguration#configure(AuthenticationManagerBuilder)");

    // 認証に DemoBasicAuthProvider を使用する
    auth.authenticationProvider(new DemoBasicAuthProvider());
  }
}
```

### AuthenticationProvider インターフェースの実装クラスを用意する

BASIC 認証の処理をするクラスを用意する。

```java
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

/**
 * BASIC 認証の簡易実装クラス。
 */
public class DemoBasicAuthProvider implements AuthenticationProvider {

  /**
   * 認証を実行する。
   *
   * @param authentication 認証リクエスト情報
   * @return クレデンシャル情報を含む認証済みの情報
   * @throws AuthenticationException 認証に失敗した場合
   */
  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    System.out.println("DemoBasicAuthProvider#authenticate");
    System.out.println(authentication.getClass().getName());

    // 入力されたユーザー名とパスワードを取得
    String inputName = authentication.getName();
    String inputPass = authentication.getCredentials().toString();

    String name = "john-doe"; // 正しいユーザー名
    String pass = "12345678"; // 正しいパスワード

    // ユーザー名とパスワードが正しいかチェック
    if (inputName.equals(name) && inputPass.equals(pass)) {
      System.out.println("ユーザー名とパスワードが正しい");
      // ユーザー名とパスワードを表現する認証済みオブジェクトを返す
      return new UsernamePasswordAuthenticationToken(inputName, inputPass, authentication.getAuthorities());
    } else {
      System.out.println("ユーザー名やパスワードが正しくない");
      throw new BadCredentialsException("ユーザー名やパスワードが正しくない");
    }
  }

  /**
   * このクラスが引数に指定された認証リクエスト情報をサポートするときは true を返す。
   *
   * @param authentication Authentication 型のクラスオブジェクト
   * @return 引数に指定された認証リクエスト情報をサポートするか
   */
  @Override
  public boolean supports(Class<?> authentication) {

    System.out.println("DemoBasicAuthProvider#supports");
    System.out.println(authentication.getName());

    // UsernamePasswordAuthenticationToken として扱える認証リクエストであれば true を返す
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
  }
}
```

## 動作例

### 認証情報を送信しなかった場合

401 Unauthorized が返される。一般的な Web ブラウザであればユーザー名とパスワードを入力するための認証用ダイアログが表示される。

```
$ curl -i -H "accept: text/html" http://localhost:8080/secret-garden/
HTTP/1.1 401 
WWW-Authenticate: Basic realm="Your secret area."
(以下略)
```

### 誤った認証情報を送信した場合

401 Unauthorized が返される。一般的な Web ブラウザであればユーザー名とパスワードを入力するための認証用ダイアログが表示される。

```
$ curl -i -H "accept: text/html" -u john-doe:aaaabbbb http://localhost:8080/secret-garden/
HTTP/1.1 401 
WWW-Authenticate: Basic realm="Your secret area."
(以下略)
```

### 正しい認証情報を送信した場合

指定した URL にコンテンツが存在するのであれば 200 OK が返される。

```
$ curl -i -H "accept: text/html" -u john-doe:12345678 http://localhost:8080/secret-garden/
HTTP/1.1 200 
(以下略)
```

## 参考資料

### Spring Secutiry 全般

- [Spring Security](https://spring.io/projects/spring-security)
- [Spring Security Reference](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/reference/htmlsingle/)
- [API Document](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/api/)
- [Hello Spring Security with Boot](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/guides/html5/helloworld-boot.html)

### WebSecurityConfigurerAdapter

- [WebSecurityConfigurerAdapter](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/api/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html)
- [Spring BootでWebセキュリティを設定しよう \(1/2\)：CodeZine（コードジン）](https://codezine.jp/article/detail/11703)
- [Spring SecurityのBasic認証をちょっぴりカスタマイズする \- Qiita](https://qiita.com/SolarSystem-7/items/4efd748a6ba0ab6910a3)

### AuthenticationProvider

- [AuthenticationProvider](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/api/org/springframework/security/authentication/AuthenticationProvider.html)

### HttpBasicConfigurer

- [HttpBasicConfigurer](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/api/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.html)

### BasicAuthenticationEntryPoint

[9\.3\. 認可 — TERASOLUNA Server Framework for Java \(5\.x\) Development Guideline 5\.5\.1\.RELEASE documentation](https://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/Security/Authorization.html)

> BasicAuthenticationEntryPoint
> Basic認証用のエラー応答を行う。
> 具体的には、HTTPレスポンスコードに401(Unauthorized)を、レスポンスヘッダとしてBasic認証用の「WWW-Authenticate」ヘッダを設定してエラー応答(HttpServletResponse#sendError)を行う。

[spring\-security/BasicAuthenticationEntryPoint\.java at 5\.2\.1\.RELEASE · spring\-projects/spring\-security · GitHub](https://github.com/spring-projects/spring-security/blob/5.2.1.RELEASE/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationEntryPoint.java)

```java
public void commence(HttpServletRequest request, HttpServletResponse response,
    AuthenticationException authException) throws IOException {
  response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
  response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
```

### AccessDeniedHandler

- [AccessDeniedHandler](https://docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/api/org/springframework/security/web/access/AccessDeniedHandler.html)
- [spring\-security/AccessDeniedHandlerImpl\.java at 5\.2\.1\.RELEASE · spring\-projects/spring\-security · GitHub](https://github.com/spring-projects/spring-security/blob/5.2.1.RELEASE/web/src/main/java/org/springframework/security/web/access/AccessDeniedHandlerImpl.java)