Spring Securityを使用したCSRF対策

## 前置き

普段Goで遊んでいるのですが、久しぶりにJavaでアプリを作りたいと思いました。せっかくなら記述方法が大幅に変更されたSpringSecurityを使ってCSRF対策を行いたいと思い、今回SpringSecurityの記事を読み漁りました。
2023年以降にバージョンアップされて書き方が変わっているのでChat-GPTも対応していないかと思います（多分）。
忘れないためにもハンズオン形式で学習してみましょう。

## 目次
- CSRFとは
- ハンズオン

## CSRFとは

「んなもん知ってるわボケ」という人は飛ばしてください。

1\. [Spring Security公式ページ](https://spring.pleiades.io/spring-security/reference/features/exploits/csrf.html#csrf-explained)
2\. [ITを分かりやすく解説](https://medium-company.com/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA/) 

上記に詳しい説明が載っていますが簡単にまとめますと、「サイトAのセッション情報やらCookieの情報を悪用して第三者が他人に成りすまして不正なリクエストを送る」というものです。

## ハンズオン
では実際にCSRF対策をSpringSecurityで行っていきましょう。
参考は[こちら](https://spring.pleiades.io/spring-security/reference/servlet/exploits/csrf.html)

まず最初にSpringBootアプリケーションを作成しましょう。
MavenでもGradleでもどちらでも構いません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3039114/7a37b4c9-4592-80e2-56c4-64cf80acaa2f.png)


次に、MavenまたはGradleにSpringSecurityを使用する旨を書きましょう。
```xml:pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
```
```gradle:build.gradle
dependencies {
	implementation "org.springframework.boot:spring-boot-starter-security"
}
```

以上で、コードを書く準備が整いました。

### MainController.java

次にリクエストの制御を行うMainControllerを作成していきます。
```Java:MainController.java
package com.example.demo;

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class MainController {
	
	// CSRF対策をしていないPOSTメソッド	
	@PostMapping("/everybody")
	public String postEverybody(){
		return "hello everybody";
	}
	
	// CSRFトークンを発行して返すメソッド
	@GetMapping("/getCsrf")
	public CsrfToken getCsrfToken(CsrfToken csrfToken) {
		return csrfToken;
	}
	
	// CSRF対策をしているPOSTメソッド
	@PostMapping("/limitedUser")
	public String postLoginUserHello() {
		return "Hello loginUser";
	}
}

```

### SecurityConfig.java

次に今回使用するSprinSecurityの設定を記述するSecurityConfigを作成しましょう。

```Java:SecurityConfig.java
package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity()
public class SecurityConfig {
    
	// CSRF認証設定
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
		
		http
			.csrf(csrf -> csrf
					.ignoringRequestMatchers(
							"/api/everybody"
							))
			.authorizeHttpRequests(auth -> auth
					.anyRequest().permitAll()
					);
		return http.build();
	}
}
```

今回はCSRFの説明だけなので、`.authorizeHttpRequests`内の説明は省きます。

まず、`.csrf()`でCSRFの設定を行います。
基本的に<font color="#ff6347">**POST, PUT, DELETE, PATCH**</font>が対象のHTTPメソッドとなります。
理由は、上記以外のHTTPメソッドはアプリケーションの状態を変更するような処理を行わないからです。

`csrf.ignoringRequestMatchers()`内にはCSRFトークンを用いた認証を行わないリクエストを記入してあげます。こちらは","で複数のリクエストを記入することができます。
今回は"/api/everybody"へのリクエストはCSRFトークンの認証を行わないように設定します。
勘のいい方は気づいたかもしれませんが、ここに書かないリクエストは基本的にCSRFトークンを使用した認証が必要になります。
では実際にPostmanで検証していきましょう。

## 検証

アプリを実行して、最初に`.ignoringRequestMatchers()`内に記述した、"/api/everybody"にリクエストを送ってみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3039114/1c88b5f4-82ed-88b8-46a8-e059e6dd84d9.png)

問題なく値が返ってきました。
"/api/everybody"のようなCSRFトークンの認証を使用しないケースとしては、ユーザーの登録やログインがあげられます。

次に、CSRFトークンの認証が必要な"/api/limitedUser"にリクエストを送ってみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3039114/72ddc39d-fbe7-990e-ea32-35f9676b9cab.png)

403エラーが返ってきました。
簡単に言うとアクセス拒否のエラーです。
「あんたはおれが発行したCSRFトークンを持っていないからリクエストを無効にするぜ」という状況です。
では、リクエストの結果が欲しいので、先にCSRFトークンを受け取りましょう。
GETで"/api/getCsrf"にリクエストを送ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3039114/2bfef9f1-bee1-1b88-5d5b-b07438c73e5b.png)

上記のようなデータが返ってきました。
"token": "～～～"が実際にサーバーから発行されたCSRFトークンとなります。
このCSRFトークンを使用して"/api/limitedUser"にリクエストを送ってみましょう。

![スクリーンショット 2023-12-21 215936.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3039114/025f4621-5184-9106-5259-a4bbd7501171.png)

"Headers"を選択し、左側のKeyに"X-CSRF-TOKEN"と入力します。その後、右側のValueに先ほど入手したCSRFトークンをコピペしてあげます。
では、"/api/limitedUser"にリクエストを送りましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3039114/a18db22b-98e8-2c68-d3fc-65c38c41e7a2.png)

無事に結果が戻ってきました。
CSRFトークンの認証を使用するケースとしては、Amazonで例えると「カートに入れる」や「アカウント情報の変更」などの、特定のユーザーのみが行える処理があがります。

## 終わりに
今回紹介したのはCSRF対策の一部なので、もう少しカスタマイズしてみたい方は[Spring Security公式ページ](https://spring.pleiades.io/spring-security/reference/servlet/exploits/csrf.html)を確認してみてください。

<br/>

**以上**