Spring Bootで、ユーザ名とパスワードを指定した認証処理の実装方法

# はじめに
Spring Bootの認証処理において、かねてから疑問に思っていたパスワードの指定方法について調べてみました。

# 概要
Spring BootとSpring Securityで、ユーザ名とパスワードを指定した認証処理を実装する方法を紹介します。

前提とする環境は以下のとおりです。

|            |Version |
|------------|---|
|Java        |1.7 |
|Spring Boot |1.5.9.RELEASE |


# 【問題】 ``UserDetailsService``ではパスワードを指定できない
「Spring Boot 認証処理」でググると、``UserDetailsService``[^fqcn-uds]の``loadUserByUsername``を使ったサンプルがたくさん見つかります。
私も書いたことがあります。 → [Spring BootでBasic認証を使用する](https://qiita.com/pale2f/items/10dd41df2da205fd576d)

しかし、以下のとおり``loadUserByUsername``の引数は``username``のみとなっています。

```java:UserDetailsService
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
```

このため、**ユーザ名とパスワードを指定した認証処理**が必要な場合には対応できません。
例えば以下のようなケースでは、``loadUserByUsername``で対応することはできません。

- パスワードのハッシュ化をDB側で実施[^cript]しており、SQLのパラメータ値にパスワードを含める必要がある
- 認証を外部ライブラリで実施しており、APIにユーザ名とパスワードのセットを渡す必要がある

[^cript]: PostgreSQLの``crypt``関数等

# 【解決策】 ``AbstractUserDetailsAuthenticationProvider``を利用する
``UserDetailsService``ではなく、``AbstractUserDetailsAuthenticationProvider``[^fqcn-audap]を利用しましょう。
具体的には、``AuthenticationProvider``のBeanを``AbstractUserDetailsAuthenticationProvider``を利用して作ることで、**ユーザ名とパスワードを利用した認証処理**を簡易に実装することができます。

## サンプルコード
```java:JavaコンフィグでAuthenticationProviderを登録
@Configuration
public class MyConfigure extends WebSecurityConfigurerAdapter {
  // ※動作確認用に、全URLをBasic認証必須とする
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic().realmName("My sample realm"); // Basic認証の設定
    http.authorizeRequests().anyRequest().authenticated(); // 認証が必要となるリクエストの設定
  }


  // ----------------------------------------
  // Bean定義
  // ----------------------------------------

  @Bean
  public AuthenticationProvider getAuthenticationProvider() {
    // 自前のAuthenticationProviderを使用する
    return new MyUserDetailsAuthenticationProvider();
  }
}
```

```java:自前のAuthenticationProviderの定義
// AbstractUserDetailsAuthenticationProviderを継承することで、retrieveUserのみを実装すればOK
public class MyUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
  private static final String DUMMY_PASSWORD = "DUMMY_PASSWORD"; // ※認証では使用しないため、値は何でもよい(nullや空文字列はNG)
  private static final List<GrantedAuthority> AUTH_USER = AuthorityUtils.createAuthorityList("USER"); // ※本サンプルでは、全員この権限とする

  // <<< N/A >>>
  @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {}
  //---

  @Override
  protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    String userId = username;
    String password = (String) authentication.getCredentials();

    // ユーザIDとパスワードをチェック
    boolean isValid = AuthApi.isValidUserIdAndPassword(userId, password); // ※外部ライブラリのAPIで認証する擬似コード
    if (!isValid) { throw new UsernameNotFoundException(username); }

    // UserDetailsの実装(User)を生成し戻り値とする
    return new User(username, DUMMY_PASSWORD, AUTH_USER);
  }
}
```

# 最後に
冒頭に書いたとおり、``loadUserByUsername``を使ったサンプルは豊富にあるのですが、なぜかパスワードを認証処理に使用するサンプルはあまり無いようでしたので調べて書いてみました。
誤りや改善点などあれば、ご指摘頂ければと思います。

[^fqcn-uds]: org.springframework.security.core.userdetails.UserDetailsService
[^fqcn-audap]: org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider