【Go】ログイン機能でウェブアプリを作ってみる（8）

こんにちは。

Part 8は「JWTについて説明してみる」です。

# このチャプターの目次
- JWTとは？
- 何に使うの？
- なんでJWTを使うの？
- 注意点
- 使用パッケージ
- JWTを作ってみた！

# JWTとは？

- Json Web Tokenの略。詳しくはjwt.ioにいくのが一番手っ取り早い
- header.payload.signatureの3つで構成される
```
例
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3MiOiAieXV5YW4iLCAiaWQiOiAiMTIzNDU2In0.rbpRDicBTBVeqLvTA-Pw1LKZyb7u8Xqid0rgSNGThDY
```
- headerは署名化の方法
    - 上記のJWTのeyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9の部分
    - これは{"alg": "HS256", "typ": "JWT"}をBase64URLエンコードし、=を取り除いたもの
- payloadはトークンの中身。idやemailなどある程度好きなものを埋め込める
    - 上記のJWTのeyJpc3MiOiAieXV5YW4iLCAiaWQiOiAiMTIzNDU2In0の部分
    - これは{"iss": "yuyan", "id": "123456"}をBase64URLエンコードし、=を取り除いたもの
- signatureはheader.payloadを鍵を使って署名化したもの。
    - 上記 JWTのrbpRDicBTBVeqLvTA-Pw1LKZyb7u8Xqid0rgSNGThDYの部分
    - headerの{"alg": "HS256", "typ": "JWT"}と、payloadの{"iss": "yuyan", "id": "123456"}を、鍵secret-keyでHMAC SHA256で署名化し、それをさらにBase64URLエンコードし、=を取り除いたもの

# 何に使うの？

- 今回は認証トークンに使う
    - payloadにidを埋め込み、認証トークンとして使う

# なんでJWTを使うの？

- 個人的な考えは、便利だから
- header, payloadの内容は簡単に取り出せる
    - base64 urlでエンコードされてるだけ
- payloadに有効期限を含めれば、フロント側で有効期限のチェックができる
- あとpayloadからidを取得すればいいので、データベースにアクセスしなくていい

# 注意点

- JWTのheader, payloadの内容は簡単に取り出せるので個人情報や秘密情報は含めない方がいい（特にパスワードをpayloadに、なんてのは絶対ダメ！）
- JWT自体が有効期限を持っている、という形なので個別ログアウトを実装するには何か仕組みが必要
    - 例えばJWTの有効期限を24時間に設定すると24時間はトークンが有効になる。
    - 全てのトークンを無効にするならコードの修正でできるけど、あるトークンだけ無効にしたい、って場合難しい（MemcachedとかRedisを使う？）

# 使用パッケージ

JWTを使う場合、既存のパッケージを使うことが多いと思います。
今回は[lestrrat-go/jwx](https://github.com/lestrrat-go/jwx)を使います。理由はissやsubのチェックがあるからです。
別に「これじゃなければダメだ！」とかないので、更新されてるか、とかスターの数とかみて好きなものを使ってください。

# JWTを作ってみた！

ごちゃごちゃ書きましたが、理解するには自分で作ってみるのが早いです。
というわけで作ってみました。

```
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"strings"
)

var (
	hs256Key    = []byte("secret-key")
	headerJSON  = `{"alg": "HS256", "typ": "JWT"}`
	payloadJSON = `{"iss": "yuyan", "id": "123456"}`
)

func main() {
	// headerをbase64URLでエンコード
	base64Header := base64.URLEncoding.EncodeToString([]byte(headerJSON))
	// base64Headerから = を取り除く
	header := strings.ReplaceAll(base64Header, "=", "")

	// payloadをbase64URLでエンコードする
	base64Payload := base64.URLEncoding.EncodeToString([]byte(payloadJSON))
	// base64Payloadから = を取り除く
	payload := strings.ReplaceAll(base64Payload, "=", "")

	// HMAC SHA256で header.payload を署名化
	mac := hmac.New(sha256.New, hs256Key)
	mac.Write([]byte(fmt.Sprintf("%s.%s", header, payload)))
	// 署名化した値をbase64URLでエンコードする
	base64Signature := base64.URLEncoding.EncodeToString((mac.Sum(nil)))
	// base64Signatureから = を取り除く
	signature := strings.ReplaceAll(base64Signature, "=", "")

	// header.payload.signatureでJWTが完成！
	jwt := fmt.Sprintf("%s.%s.%s", header, payload, signature)

	fmt.Println(jwt)
}
```
```
func decodeJWT(token string, hs256Key []byte) bool {
	// jwtをheader, payload, signatureに分ける
	arr := strings.Split(token, ".")
	
	if len(arr) != 3 {
		fmt.Println("invalid token")
		return false
	}
	header := arr[0]
	payload := arr[1]
	signature := arr[2]

	mac := hmac.New(sha256.New, hs256Key)
	mac.Write([]byte(fmt.Sprintf("%s.%s", header, payload)))
	verify := strings.ReplaceAll(base64.URLEncoding.EncodeToString(mac.Sum(nil)), "=", "")

	if !hmac.Equal([]byte(signature), []byte(verify)) {
		fmt.Println("invalid hs256key")
		return false
	}
	return true
}
```

こんな感じです。

今日は以上です。
ありがとうございました。

次のPart 9は「ログイン JwtBuilder」です。
よろしくお願いいたします。