JWTについて調べてみた

# はじめに
JWTって聞いたことあるけど、詳しく知らないと思い、調べてみることにしました。
基本的なことを調べ、SpringSecurityとauth0:java-jwtを使い実際に簡単な実装をしてみたいと思います。

# 目次
1. [JWTって何？](#JWTって何？)
1. [どう認証するのか？](#どう認証するのか)
1. [簡単に実装してみた](#簡単に実装してみた)
    1. [実装](#実装)
    1. [検証](#検証)
1. [まとめ](#まとめ)
1. [参考文献](#参考文献)

# JWTって何？
JWTとはJsonWebTokenの略称であり、以下のようなJsonをBase64エンコードなどでURLセーフにしたものです
あくまでJsonの表示形式を決めただけのもので、これ自体が認証するわけではないありません
```
{
  "iss": "jwt-demo-server",
  "sub": "1",
  "jti": "89ce044c-6fbe-4e99-b465-089e0d5c2aba",
  "iat": 1722736918,
  "exp": 1722740518
}
```
このJsonのキーの説明
|キー名  |説明  |
|---|---|
|iss |JWTの発行者  |
|sub |ユーザの識別子など  |
|jti |JWTの一意な識別子 |
|iat |JWTの発行日時 |
|exp |JWTの有効期限 |

# どう認証するのか？
JWS（JsonWebSignature）というJWTから作る検証シグネチャによって認証を行います
JWSは以下のように作成されます
まずは暗号アルゴリズムとペイロード（JWT）の種別を表すヘッダーを用意します
```
{
  "alg": "HS256",
  "typ": "JWT"
}
```
次にURLエンコードしたヘッダーとJWTを暗号キーで暗号化したシグネチャを作成します
```
シグネチャ = 「{ヘッダと言われるJsonをURLエンコードしたもの} . {JWT}」を暗号化キーで暗号化
```

それぞれ作成したものを「.」で繋いだものがJWS
```
JWS = {ヘッダと言われるJsonをURLエンコードしたもの} . {JWT} . {シグネチャをURLエンコードしたもの}
```

# 簡単に実装してみた
##### 実装
今回はauth0:java-jwtとSpring Securityを使って認証機能を作っていきます
以下認証までの流れです
```
1. /auth/login にemailとpasswordでリクエストを送る
2. LoginControllerでリクエストを受け取り、emailとpasswordで認証。ユーザIDを含んだJWSを作成し、返却する。
3. /hello のHeaderに発行したJWSをつけ、リクエストを送る
4. filterでJWSで認証する
5. "Hello, World!"が返される
```

**build.gradleにライブラリを追加**
```
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.auth0:java-jwt:4.4.0'
 ```

**JwtUtilsを作成**
ここでJWSの作成・認証を行います
本来なら暗号化キーはもっと厳重に管理した方が良いですが、今回は簡単な実装なためコードにベタ書しています。
```java:JwtUtils
public class JwtUtils {
    private static final Long EXPIRATION_TIME = 1000L * 60L * 60L;
    private static final String SECRET_KEY = "secret";

    public static String build(Integer userId) {
        Date issuedAt = new Date();
        Date expiresAt = new Date(issuedAt.getTime() + EXPIRATION_TIME);

        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);

        return JWT.create()
                .withIssuer("jwt-demo-server")
                .withSubject(userId.toString())
                .withJWTId(UUID.randomUUID().toString())
                .withIssuedAt(issuedAt)
                .withExpiresAt(expiresAt)
                .sign(algorithm);
    }

    public static DecodedJWT verify(String token) {
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer("jwt-demo-server")
                .build();

        return verifier.verify(token);
    }
}
```

**LoginControllerを作成**
受け取ったemailとpasswordで認証し、ユーザIDを取得します。
そのユーザIDを利用し、JWSを作成します。
```java:LoginController
@RestController
@RequestMapping("/auth")
public class LoginController {
    @Resource
    AuthService authService;

    @PostMapping("/login")
    public ResponseEntity login(@RequestBody LoginRequest request) {
        // emailとpasswordで認証し、ユーザID（1）を返す
        Integer userId = authService.auth(request.getEmail(), request.getPassword());

        String token = JwtUtils.build(userId);

        return ResponseEntity.ok(token);
    }
}
```

**SecurityConfigを作成**
今回はpostmanなどでAPIを直接叩いたので、csrfの設定を無効にしておくきます。
```java:SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }
}
```

**AuthorizeFilterを作成**
OncePerRequestFilterというリクエストごとに毎回呼び出すクラスを継承し作成します。
matcherに合致しないリクエストのみを認証対象とします。
JWSがない or 認証できなければ401が返されます。
```java:AuthorizeFilter
@Component
public class AuthorizeFilter extends OncePerRequestFilter {

    private final AntPathRequestMatcher matcher = new AntPathRequestMatcher("/auth/login");

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (!matcher.matches(request)) {
            String authorizationHeader = request.getHeader("Authorization");

            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                String token = authorizationHeader.replace("Bearer ", "");

                try {
                    DecodedJWT decodedJWT = JwtUtils.verify(token);
                } catch (Exception e) {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
            } else {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }

        filterChain.doFilter(request, response);
    }
}
```

**HelloControllerを作成**
```java:HelloContorller
@RestController
@RequestMapping("/hello")
public class HelloController {
    @GetMapping("")
    public String hello() {
        return "Hello, World!";
    }
}
```

##### 検証
まずは/auth/login にリクエストを送り、JWSを取得します
![Cursor_と_ログイン_-_My_Workspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1245466/48162067-b324-de5e-fcc1-314f6623ed40.png)

次に/hello へJWSをつけてリクエストを送ると、Hello, World!と返ってきます
![return_hello_-_My_Workspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1245466/2e4159f4-e951-64cc-bd3f-cb73f21b42dc.png)

JWSを持たせずにリクエストを送ると、401となります
![return_hello_-_My_Workspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1245466/e3631c62-6b84-0618-bb71-bfa6f7be9aca.png)

# まとめ
今回はJWTについて調べ、それを使って簡単に認証機能を作ってみました。
JWTとして送る情報によって、認可処理とかも入れることができるようなので、次は入れてみたいと思います。

# 参考文献
- [基本から理解するJWTとJWT認証の仕組み](https://developer.mamezou-tech.com/blogs/2022/12/08/jwt-auth/)
- [Spring Boot × SPAで作成したアプリケーションに、 Spring Securityを用いた認証機能を導入する](https://qiita.com/takech111/items/44a703a05ea85962bca6)