Spring BootでBasic認証を使用する

# はじめに
何となく、SIer向けの文体で書いてみました。環境も、あえてJava１.7にしてます。

# 概要
Spring BootでBasic認証を方法をサンプルコードを用いて説明する。
WebAPIとしての利用シーンを想定し、ステートレスな通信に対応した設定とする。

# 前提とする環境

## バージョン

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

## 依存関係

- Spring Security


# サンプルコード

## 設定

JavaConfigで以下の設定をする。具体的な設定内容についてはソースコード中のコメントを参照すること。

- Spring Securityの設定
    - Basic認証
    - ステートレス通信のための設定
- Bean定義
    - userDetailsService
    - passwordEncoder

```java:MyConfigure.java
@Configuration
public class MyConfigure extends WebSecurityConfigurerAdapter {

  // ----------------------------------------
  // Spring Securityの設定
  // ----------------------------------------

  // <<< ※WebSecurityConfigurerAdapterには、configureのオーバーロードメソッドが複数あるので要注意 >>>
  @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); }
  @Override public    void configure(WebSecurity                  web)  throws Exception { super.configure(web); }
  //---

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // Basic認証の設定
    http.httpBasic().realmName("My sample realm");
    // 認証が必要となるリクエストの設定
    http.authorizeRequests().anyRequest().authenticated();
    // CSRF対策が有効だとTokenなしのPOSTがエラーとなるため、無効化する
    http.csrf().disable();
    // 認証情報は常にAuthorizationヘッダから取得するため、Cookieによるセッション管理は不要
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  }

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

  @Bean
  public UserDetailsService userDetailsService() {
    return new LoginPrincipal.LoginPrincipalService();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

}
```

## 認証情報

認証情報は、`UserDetails`[^fqdn-1]を実装したクラスとして定義する(`LoginPrincipal`)。
このクラスは、以下の点に注意して設計する。

- 認証に必要な情報のみを保持するようにする
    - 業務的に必要となる「利用者」情報は、このクラスとは別に定義する
- `UserDetails`における*username*の意味を明確にする

上記を踏まえて本サンプルでは、以下の設計とする。

- *username*には、「ログインID」を保持する
- `UserDetails`の情報に加え「従業員番号」を保持する

`UserDetailsService`[^fqdn-2]の実装クラスをBean定義することで、Spring Securityが認証情報を検索する際の実装を定義できる(`LoginPrincipal.LoginPrincipalService`)。
このクラスは、コード簡略化のため`LoginPrincipal`の内部クラスとして定義している。

なお、`LoginPrincipal.DB`クラスには、サンプルコード単体で動作可能とするためのダミー処理が定義されている。

```java:LoginPrincipal.java
// ======================================================================
// ※UserDetailsの実装クラス
// ======================================================================
public class LoginPrincipal extends org.springframework.security.core.userdetails.User {
  // ※loginId(ログインID)はsuper.usernameに保持する
  private final String employeeNumber; // 従業員番号

  public LoginPrincipal(String loginId, String employeeNumber, String encodedPassword, String[] roles) {
    super(loginId, encodedPassword, true, true, true, true, AuthorityUtils.createAuthorityList(roles));
    this.employeeNumber = employeeNumber;
  }

  public String getLoginId() {
    return super.getUsername();
  }

  // <<< Getter >>>
  public String getEmployeeNumber() { return this.employeeNumber; }
  //---

  // ======================================================================
  // ※UserDetailsServiceの実装クラス
  // ======================================================================
  public static class LoginPrincipalService implements org.springframework.security.core.userdetails.UserDetailsService {

    /**
     * 指定されたログインIDで認証情報を検索します。
     * 認証情報が見つからなかった場合、結果は{@code null}です。
     */
    public LoginPrincipal findByLoginId(String loginId) {
      // ※実際には、ここで認証情報をDBから取得する
      return DB.AUTH_TABLE.get(loginId);
    }

    /** {@inheritDoc} */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      // ※usernameには、Spring Securityの認証機能により基本認証の「ユーザ名」が設定されている
      // findByLoginIdに委譲
      LoginPrincipal found = this.findByLoginId(username);
      if (found == null) {
        throw new UsernameNotFoundException("username not found: " + username);
      }
      return found;
    }
  }


  // ======================================================================
  // (サンプル用) ユーザ認証マスタの代替として、Mapにユーザ認証情報を保持する
  // ======================================================================
  private static class DB {
    public static final Map<String, LoginPrincipal> AUTH_TABLE = new HashMap<>();

    static {
      BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
      LoginPrincipal[] data = { //
          new LoginPrincipal("U001", "S000001", passwordEncoder.encode("pass1"), new String[] { "USER" }), //
          new LoginPrincipal("U002", "S000002", passwordEncoder.encode("pass2"), new String[] { "USER" }), //
      };
      for (LoginPrincipal d : data) {
        AUTH_TABLE.put(d.getLoginId(), d);
      }
    }
  }

}
```

以上でBasic認証が有効になる。
Controllerから認証情報を参照するサンプルを以下に挙げる。

```java:MyController.java
@RestController
public class MyController {

  @GetMapping
  public String index() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    LoginPrincipal loginPrincipal = (LoginPrincipal) securityContext.getAuthentication().getPrincipal();

    return "Hello " + loginPrincipal.getEmployeeNumber() + "!";
  }

}
```


# おわりに
初めてのQiita投稿です。続けられるといいなと思います。
ご指導ご鞭撻のほどお願いいたします。


[^fqdn-1]: org.springframework.security.core.userdetails.UserDetails
[^fqdn-2]: org.springframework.security.core.userdetails.UserDetailsService