【逆引き】Spring Security（随時更新）

# 概要

Spring Securityの利用にあたり、逆引きという形でまとめました。ここに載っていることが正解ではなく、あくまでもひとつの方法ですので実装に迷ったときに参考にしていただければ幸いです。

リファレンスは随時更新していきます。

# 環境
|項目|バージョン|
|:--|:--|
|Java|8|
|Spring Boot|2.2.4|

# 準備

以下は最低限必要なコードです。

```java:全てのリクエストを制限

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
    }
}
```

今回はDBに接続しないので、以下の認証情報を利用します。

```yml:application.yml
spring:
    security:
        user:
            name: user
            password: pass
            roles: USER
```

# 注意事項

- コード中に(A)、(B)など出てきますが、これは同じものが入ることを意味します。スコープは各リファレンス上のみとなります。
- 画面表示用のコントローラークラス（```@Controller```）は割愛させていただきますので、適宜作成してください。

# 逆引き
## 共通編
### 特定のパスは制限をかけたくない

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/","/login") // 「/」「/login」は認証不要でアクセス可能
                .permitAll()
                .anyRequest()
                .authenticated();
    }
}
```

### CSRFを無効にしたい

デフォルトでCSRFが有効になっている。

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
        http.csrf().disable();
    }
}
```

### 外部（オリジンが違うサイト）からのリクエストを受け付けたい

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
        http.cors().configurationSource(getCorsConfigurationSource());
    }

    private CorsConfigurationSource getCorsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 全てのメソッドを許可
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        // 全てのヘッダを許可
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        // 全てのオリジンを許可
        corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);

        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        // パスごとに設定が可能。ここでは全てのパスに対して設定
        corsSource.registerCorsConfiguration("/**", corsConfiguration);

        return corsSource;
    }
}
```

### 認証処理をカスタマイズしたい

```AuthenticationProvider``` の実装クラスを作成します。

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

    // 認証処理
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        if (!"user".equals(username) || !"password".equals(password)) {
            throw new BadCredentialsException("ログイン情報が間違っています");
        }
        return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
    }

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

}
```

```SecurityConfig``` に上記で作成したProviderを指定します。

```java:SecurityConfig.java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomeAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();

        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 上記で作成したProviderを指定
        auth.authenticationProvider(authenticationProvider);
    }
}
```


## Form認証編

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
        http.formLogin();
    }
}
```

```/login``` にアクセスすると、Spring Securityで用意されたログイン画面が表示される。

### 独自のログイン画面を利用したい

ログイン画面のHTMLを用意します。

```html:login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ログイン</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input type="text" name="username">
        <input type="password" name="password">
        <button type="submit">ログイン</button>
    </form>
</body>
</html>
```

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("(A)") // 追加
                .permitAll()         // 追加
                .anyRequest()
                .authenticated()
        http.formLogin()
                .loginPage("(A)");  // 追加
    }
}
```

### 認証用のパラメータを変更したい

デフォルトでは、```username```と```password```というパラメータ名で取得します。

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/(A)") // ①のパス
                .permitAll()
                .anyRequest()
                .authenticated()
        http.formLogin()
                .loginPage("/(A)")
                .usernameParameter("(B)")  // 追加
                .passwordParameter("(C)"); // 追加
    }
}
```
```html:login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ログイン</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input type="text" name="(B)">
        <input type="password" name="(C)">
        <button type="submit">ログイン</button>
    </form>
</body>
</html>
```

### 認証成功時に遷移する画面を変更したい

デフォルトでは「/」に遷移する。

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
        http.formLogin()
                .defaultSuccessUrl("/home"); // 「/home」に遷移
    }
}
```


### ３つ以上のパラメータで認証したい

通常、```username```、```password```という２つのパラメータで認証されます。
方法はいくつかありますが、ここではForm認証における方法です。

#### 作成するもの
- ログイン画面
- 認証情報格納クラス
- 認証プロバイダ
- 認証フィルター

３つのパラメータを入力するログイン画面を用意します。

```html:login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ログイン</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input type="text" name="(A)">
        <input type="text" name="username">
        <input type="password" name="password">
        <button type="submit">ログイン</button>
    </form>
</body>
</html>
```

認証情報を保持する```UsernamePasswordAuthenticationToken```を継承したクラスを作成します。

```java:MultiParamAuthenticationToken
public class MultiParamAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private Object tenant;　// 追加パラメータ

    public MultiParamAuthenticationToken(Object principal, Object credentials, Object tenant) {
        super(principal, credentials, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
        this.tenant = tenant; 
    }

    public Object getTenant() {
        return this.tenant;
    }
}
```

認証プロバイダークラスを作成します。

```java:MultiParamAuthenticationProvider

@Configuration
public class MultiParamAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        String tenant = null;
        if (authentication instanceof MultiParamAuthenticationToken) {
            tenant = (String) ((MultiParamAuthenticationToken) authentication).getTenant();
        }
        if (!"user".equals(username) || !"pass".equals(password) || !"multi".equals(tenant)) {
            throw new BadCredentialsException("aaa");
        }
        return new MultiParamAuthenticationToken(username, password, tenant);
    }

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

}
```

３つ目のパラメータを取得するためのフィルタークラスを用意します。

```java:MultiParamAuthenticationFilter
public class MultiParamAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

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

        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String tenant = obtainTenant(request);

        MultiParamAuthenticationToken authRequest = new MultiParamAuthenticationToken(
                username, password, tenant);

        return getAuthenticationManager().authenticate(authRequest);
    }

    private String obtainTenant(HttpServletRequest request) {
        return (String) request.getParameter("tenant");
    }
}
```

```java:SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MultiParamAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login")
                .permitAll()
                .anyRequest()
                .authenticated();

        // formLoginを利用せずに独自フィルターを設定
        http.addFilter(getMultiParamAuthenticationFilter());
    }

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

    private MultiParamAuthenticationFilter getMultiParamAuthenticationFilter() throws Exception {
        MultiParamAuthenticationFilter filter = new MultiParamAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }
}
```

## OAuth認証編

OAuth認証を行うにあたり、認証プロバイダでクライアントIDとシークレットを発行しておく必要があります。
ここではGoogleでOAuth認証を行います。


### 最低限必要な実装

pom.xmlに以下の依存関係を追加します。

```xml:pom.xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
```

application.ymlにOAuth情報を設定します。

```yaml:application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            clientId: <クライアントID>
            clientSecret: <クライアントシークレット>
```

```java:SpringSecurity.java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();

        http.oauth2Login(); // 追加
    }
```

### 認証時の情報を利用したい

```java:LoginController.java
@Controller
public class LoginController {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    @GetMapping
    public String index(OAuth2AuthenticationToken authentication, Model model) {

        OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
                authentication.getAuthorizedClientRegistrationId(),
                authentication.getName());

        model.addAttribute("name", authorizedClient.getPrincipalName());
        model.addAttribute("accessToken", authorizedClient.getAccessToken().getTokenValue());

        return "index";
    }
```

```OAuth2AuthenticationToken``` で認証情報が取得できます。

次に、表示用の画面を作成します。

```html:index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ホーム</title>
</head>
<body>
    <p th:text="${accessToken}"></p>
    <p th:text="${name}"></p>
</body>
</html>
```

### 独自のログイン画面でOAuth認証したい

ログイン画面を作成します。

```html:login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ログイン</title>
</head>
<body>
    <a href="/oauth2/authorization/google">Google認証</a>
</body>
</html>
```

```java:SecurityConfig.java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("(A)")
                .permitAll()
                .anyRequest()
                .authenticated();

        http.oauth2Login().loginPage("(A)"); // 追加
    }
```

## 独自認証編

### JSONによる認証を行いたい

```UsernamePasswordAuthenticationFilter``` を継承したクラスを作成します。

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

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

        try {
            // パラメータを取得
            // 認証パラメータ情報がrequest.getInputStream()に格納されているので、Jacksonを利用して取り出している
            Map<String, String> params = new ObjectMapper().readValue(request.getInputStream(),
                    new TypeReference<Map<String, String>>() {});

            // 認証リクエスト情報生成
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    params.get("(A)"), params.get("(B)"));

            // 認証
            return getAuthenticationManager().authenticate(authRequest);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 認証成功時に処理を行う
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
            HttpServletResponse res,
            FilterChain chain,
            Authentication auth) throws IOException, ServletException {
        // 認証済みユーザを保存
        // 保存しないと、未ログイン扱いになる
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
}
```

ログイン画面はAjax通信で認証情報を送信するようにします。

```html:login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ログイン</title>
</head>
<body>
    <form>
        <input type="text" id="(A)" name="(A)">
        <input type="password" id="(B)" name="(B)">
        <button type="button" id="btnLogin" onclick="login()">ログイン</button>
    </form>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
        function login() {
            // 認証パラメータを取得
            const data = {
                email: document.getElementById('(A)').value,
                password: document.getElementById('(B)').value
            }
            // 認証
            axios.post('(C)', data)
                .then(res => location.href = "/home") // 認証成功時は「/home」に遷移する
        }
    </script>
</body>

</html>
```

上記で作成したFilterをSecuritConfigで設定します。

```java:SecurityConfig.java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("(C)")
                .permitAll()
                .anyRequest()
                .authenticated();

        // Ajax通信を行うので無効にしておく
        http.csrf().disable();

        // Jsonによる認証フィルターを設定
        http.addFilter(getJsonAuthenticationFilter());
    }

    private JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        // 認証方法はデフォルトを利用
        filter.setAuthenticationManager(authenticationManager());
        // Filterが実行されるパスとHTTPメソッドを指定
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("(C)", "POST"));
        return filter;
    }
}
```

## 応用編

### 認証後JWTを返却したい

pom.xmlにJWTのライブラリを追加します。

```xml:pom.xml（追加分のみ）
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
```

認証情報をJSONで送信し、成功後JWTを返却します。

```UsernamePasswordAuthenticationFilter``` を継承したクラスを作成します。

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

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

        try {
            // パラメータを取得
            // 認証パラメータ情報がrequest.getInputStream()に格納されているので、Jacksonを利用して取り出している
            Map<String, String> params = new ObjectMapper().readValue(request.getInputStream(),
                    new TypeReference<Map<String, String>>() {});

            // 認証リクエスト情報生成
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    params.get("(A)"), params.get("(B)"));

            // 認証
            return getAuthenticationManager().authenticate(authRequest);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 認証成功時にJWTの作成を行い、レスポンスヘッダに設定する。
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
            HttpServletResponse res,
            FilterChain chain,
            Authentication auth) throws IOException, ServletException {

        Date issuedAt = new Date();
        Date notBefore = new Date(issuedAt.getTime());
        Date expiresAt = new Date(issuedAt.getTime() + TimeUnit.MINUTES.toMillis(100L));

        // JWT生成
        String token = JWT.create()
                .withIssuedAt(issuedAt)
                .withNotBefore(notBefore)
                .withExpiresAt(expiresAt)
                .withClaim("(C)", (String)auth.getPrincipal())
                .sign(Algorithm.HMAC512("secret"));

        // AuthorizationヘッダにJWTを設定
        res.addHeader("Authorization", "Bearer " + token);
    }
}
```

```OncePerRequestFilter``` を継承したJWTを検証するクラスを作成します。

```java:JwtAuthenticationFilter.java
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {

        String header = req.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            // 処理を続行しますが、SecurityContextに認証情報を設定しないので、結果的に403が返却されます
            chain.doFilter(req, res);
            return;
        }

        // AuthorizationヘッダのBearer Prefixである場合
        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        if (Objects.isNull(authentication)) {
            chain.doFilter(req, res);
            return;
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);

        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return null;
        }
        token = token.substring("Bearer ".length());

        if (Objects.isNull(token) || token.length() == 0) {
            return null;
        }

        JWTVerifier verifier = JWT.require(Algorithm.HMAC512("secret")).build();

        try {
            DecodedJWT jwt = verifier.verify(token);

            return new UsernamePasswordAuthenticationToken(
                    jwt.getClaim("(C)").asString(), null,
                    null);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

}
```

```java:SecurityConfig.java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login")
                .permitAll()
                .anyRequest()
                .authenticated();

        http.csrf().disable();

        http.addFilter(getJsonAuthenticationFilter());

    }

    private JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        // 認証方法はデフォルトを利用
        filter.setAuthenticationManager(authenticationManager());
        // Filterが実行されるパスとHTTPメソッドを指定
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        return filter;
    }
}
```