1. トークンについて
今回実装するにあたりアクセストークンとリフレッシュトークンを使用しました。
アクセストークンは有効な時間が短く、このトークンをヘッダーに含めてリクエストを送ります。バックエンドで処理を行う前にアクセストークンの認証をし、リクエストを送ったユーザーがログイン済みのユーザーか判別します。リフレッシュトークンはアクセストークンを送った際に期限が切れていた時に、リフレッシュトークンを送ります。この時リフレッシュトークンも有効な時間が制限されており、有効だった場合は認証が通りアクセストークンを再発行できるようにします。もしリフレッシュトークンも有効でなかった場合ユーザーはログインしなおす必要があります。
2. ログイン機能のフローについて
今回自分が実装したログイン機能の簡単なフローを載せておきます
- ユーザー名、パスワードをbodyに含めてリクエストを送る。
- データベースからリクエストのユーザー名と一致するユーザーインスタンスを取得する
- ユーザーインスタンスのパスワードとリクエストのパスワードが一致したらトークン発行へ
- アクセストークン、リフレッシュトークンを作成し、リフレッシュトークンはユーザーインスタンスに格納する
- リフレッシュトークンとアクセストークンをレスポンスとして返す。
以上が今回自分が実装したログイン機能の簡単なフローです
3. 具体的なトークン周りの解説
以下が自分が実装した実際のコードです。
var accessSecret = []byte(os.Getenv("ACCESS_SECRET_KEY"))
var refreshSecret = []byte(os.Getenv("REFRESH_SECRET_KEY"))
func (s *AuthService) GenerateTokens(username string) (accessToken, refreshToken string, err error) {
user, _ := s.userRepository.FindByUsername(username)
accessClaims := jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(time.Minute * 15).Unix(),
}
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(accessSecret)
if err != nil {
return "", "", err
}
// リフレッシュトークン
refreshClaims := jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 有効期限7日
}
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(refreshSecret)
if err != nil {
return "", "", err
}
err = s.userRepository.CreateAccessToken(user, refreshToken)
if err != nil {
return "", "", err
}
return accessToken, refreshToken, nil
}
JWTは以下のように構成されています。
<header>.<payload>.<signature>
この時、JWT生成時にheaderのデータは変更されずにpayloadが動的なので、異なるトークンを生成できるのはpayloadが動的だからです。そしてsignatureはpayloadに依存しているので、signatureも動的です。
上記のコードでpayloadに該当するのはaccessClaimsとrefreshClaimsです。ここで動的な理由はコードを見ていただけると分かる通り、有効期限がデータに含まれていて、その有効期限はその時点の時間が関係しているので、payloadが動的だとわかります。
4. アクセストークンはサーバー側で保存しなくて良いのか
実装している時にリフレッシュトークンはデータベースに保存しているのにアクセストークンは保存しないことに疑問を持ったので簡単に解説します。
そもそも何でデータベースに保存しようと思ったのか
トークンを使用する理由はユーザーがログインしているか判別するためです。
リクエストを送ってきたユーザーに対して正常なユーザーであると認証しなければなりません。ここで自分は発行したトークンをサーバー側でも保存しておき、ユーザーのリクエストに含まれるトークンと照合して認証すれば良いのではないかと考えました。なので認証する上でトークンをこちらでも持っておかないと認証できないのではないかと思ってしました。
なぜサーバー側でトークンを保存しなくても認証できるのか
ここで重要なのがシークレットキーです。トークンのsignatureの部分はheaderとpayloadとシークレットキーを用いて作成されます。ですが、トークンにはシークレットキーについての情報は載っていません。ではどうやって検証するのでしょうか。
それはリクエストで送ってきたトークンからheaderとpayloadを取り出します。このheaderとpayloadとシークレットキーからトークンを作成します。このトークンと送ってきたトークンを比較して検証できます。
5. 最後に
今回ブログを作成してみて、情報をうまくまとめたり構成することはとても難しく感じました!
読みにくい部分はあると思いますがご容赦ください
もしこのブログ上で間違えた解釈がありましたらご連絡していただけるとありがたいです!
次回はshimaさんです!お楽しみに!