Spring Securityを使って、３つ以上のパラメータをJSON形式で認証する

# はじめに

Spring Securityでは、デフォルトでForm認証で、パラメータ名が```username```、```password```となっている。そんなに凝ったことしなくてもいいのであれば、デフォルトの認証機構を使うことで、コード量を減らせるが、最近だとJSON形式による認証も増えてきているので、Spring Securityでどうカスタマイズしたらいいのか色々調べてみた。

# 認証パラメータ

今回の認証を行うためのパラメータは以下の通り。

|パラメータ名|内容|
|:--|:--|
|email|メールアドレス|
|password|パスワード|
|tenantCode|テナントコード|

# 手順

### 1. 認証用のパラメータを格納するクラスを用意

```java:Principal.java
@Getter
@Setter
public class Principal {
  private String email;
  private String tenantCode;
  private String password;
}
```

### 2. ログインユーザ情報を格納するクラスを用意

```java:LoginUser.java
@Getter
@Setter
public class LoginUser extends org.springframework.security.core.userdetails.User{
  private long userId;
  private String userName;
  // 他に保持したいプロパティがあれば追加
}
```

### 3. 認証の前処理を行うフィルターを用意

```java:CustomAuthenticationFilter.java
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) {

    try {
      Principal principal = new ObjectMapper().readValue(request.getInputStream(),
          Principal.class);

      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
          principal, null);

      setDetails(request, authRequest);

      return this.getAuthenticationManager().authenticate(authRequest);

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest req,
      HttpServletResponse res,
      FilterChain chain,
      Authentication auth) {

    SecurityContextHolder.getContext().setAuthentication(auth);

  }
}
```

### 4. 認証処理を行うクラスを用意

```java:CustomAuthenticationProvider.java
@Configuration
public class CustomAuthenticationProvider implements AuthenticationProvider {

  // ユーザ情報リポジトリ
  @Autowired
  UserRepository userRepository;

  // テナント情報リポジトリ
  @Autowired
  TenantRepository tenantRepository;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    // CustomAuthenticationFilterクラスから渡されたものを取得
    Principal principal = (Principal) authentication.getPrincipal();

    if (principal == null) {
      throw new BadCredentialsException("認証情報がありません");
    }

    // テナントコードの有効チェック
    Tenant tenant = tenantRepository.findByTenantCode(principal.getTenantCode());
    if (tenant == null || tenant.getStatus() == 0) {
      throw new BadCredentialsException("テナントが無効です");
    }

　　　　　　　　// メールアドレスとテナントコードでユーザの有効チェック
    User user = userRepository.findByEmailAndTenantId(principal.getEmail(), tenant.getId());

    // ユーザー情報を取得できなかった場合
    if (user == null) {
      throw new BadCredentialsException("ユーザが存在しません");
    }

    if (!new BCryptPasswordEncoder().matches(principal.getPassword(), user.getPassword())) {
      throw new BadCredentialsException("パスワードが間違っています");
    }

    List<GrantedAuthority> authorityList = new ArrayList<>();

    // 権限付与処理

　　　　　　　　LoginUser　loginUser = new LoginUser();
    loginUser.setUserId(user.getId());
    loginUser.setUserName(user.getUserName());

    return new UsernamePasswordAuthenticationToken(loginUser, principal.getPassword(),
        authorityList);
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
  }

}

```

### 5. ```WebSecurityConfigurerAdapter```の継承クラスを用意

```java:SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  CustomAuthenticationProvider authenticationProvider;

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

    CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
    filter.setRequiresAuthenticationRequestMatcher(
        new AntPathRequestMatcher("<ログインのURL>", "POST"));
    filter.setAuthenticationManager(authenticationManagerBean());

    http
        .csrf()
        .disable()
        .authorizeRequests()
        .antMatchers("<認証不要なURL>")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .logout()
        .logoutRequestMatcher(
            new AntPathRequestMatcher("<ログアウトのURL>", "POST"))
        .and()
        .addFilter(filter);
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider);
  }
}
```

# 参考

- [Spring Securityで独自の認証を実装する](https://fueiseix.hatenablog.com/entry/2018/03/11/191200)
- [Spring Securityのカスタマイズ、どこでなにするのか？のおぼえがき](https://wannabe-jellyfish.hatenablog.com/entry/2018/01/10/013508)