Go でJWT認証を実装
はじめに
GoでJWT認証の実装方法を紹介します。
ライブラリ
使用ライブラリは雨下の通りです。
-
GORM (Postgres driver): v1.5.9
ORMライブラリで、PostgreSQLデータベースに対するオブジェクト操作を簡素化する。 -
Gin: v1.10.0
Go言語の高性能なWebフレームワークで、APIやWebアプリケーションの開発に利用する。 -
bcrypt: v0.27.0
ユーザーのパスワードを安全に保存するために、パスワードのハッシュ化を行う。 -
JWT (JSON Web Tokens): v5.2.1
認証で使用するJWTトークンの生成、解析、検証を行うライブラリ。 -
Godotenv: v1.5.1
.envファイルから環境変数をロードし、開発環境での設定管理を容易にする。 -
CompileDaemon: v1.4.0
ファイル変更を監視し、自動でGoアプリケーションのビルドと再起動を行う開発支援ツール。
実装
今回、実装するAPIは下記の3つ
-
サインアップ
ユーザ名(メールアドレス)とパスワードを保存する。 -
ログイン
ログイン処理。IDと有効期限をJWTトークンとして生成し、クッキーに保存 -
認証
クッキーからトークンを取得し、ユーザ情報を解析
コード
パッケージ構成は下記の通り
package main
import (
"fmt"
"example.com/go-jwt/controllers"
"example.com/go-jwt/initializers"
"example.com/go-jwt/middleware"
"github.com/gin-gonic/gin"
)
func init() {
initializers.LoadEnvVariables()
initializers.ConnectToDB()
initializers.SyncDatabase()
}
APIを定義。Validateメソッドを呼び出す前にRequireAuthを呼び出して認証を行わせる
func main() {
r := gin.Default()
r.POST("/signup", controllers.SignUp)
r.POST("/login", controllers.Login)
r.GET("/validate", middleware.RequireAuth, controllers.Validate)
r.Run()
fmt.Println("Hello, World!")
}
コントローラーと言いつつ、ログイン、サインアップの実装内容を記述
package controllers
import (
"net/http"
"os"
"time"
"example.com/go-jwt/initializers"
"example.com/go-jwt/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
func SignUp(c *gin.Context) {
var body struct {
Username string
Password string
}
if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "パラメータが存在しません。",
})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "パスワードのハッシュ化に失敗しました。",
})
return
}
user := models.User{Username: body.Username, Password: string(hash)}
result := initializers.DB.Create(&user)
if result.Error != nil {
c.JSON(500, gin.H{
"error": "ユーザーの作成に失敗しました。",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ユーザーの作成に成功しました。",
})
}
func Login(c *gin.Context) {
var body struct {
Username string
Password string
}
if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "パラメータが存在しません。",
})
}
var user models.User
initializers.DB.First(&user, "username = ?", body.Username)
if user.ID == 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "ユーザーが存在しません。",
})
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "パスワードが間違っています。",
})
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID,
// 有効期限を1日に設定
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "トークンの作成に失敗しました。",
})
}
c.SetSameSite(http.SameSiteLaxMode)
// クッキーにトークンをセット(キー, 値, 有効期限, パス, ドメイン, https, httponly)
c.SetCookie("Authorization", tokenString, 3600, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{
"token": tokenString,
})
}
func Validate(c *gin.Context) {
user, _ := c.Get("user")
c.JSON(http.StatusOK, gin.H{
"message": "ログイン済み:" + user.(models.User).Username,
})
}
認証の実装。トークンを解析して、IDと有効期銀の確認。
コンテキストに取得したユーザ情報をセットして次の後続メソッドに情報を渡す
package middleware
import (
"log"
"net/http"
"os"
"time"
"example.com/go-jwt/initializers"
"example.com/go-jwt/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func RequireAuth(c *gin.Context) {
tokenString, err := c.Cookie("Authorization")
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// .envからシークレットキーを取得
secret := []byte(os.Getenv("SECRET"))
return secret, nil
})
if err != nil {
log.Fatal(err)
} else if claims, ok := token.Claims.(jwt.MapClaims); ok {
if time.Now().Unix() > int64(claims["exp"].(float64)) {
// トークンが期限を過ぎている
c.AbortWithStatus(http.StatusUnauthorized)
}
// "sub"に入れておいたIDをもとにユーザー情報を取得
var user models.User
initializers.DB.First(&user, claims["sub"])
if user.ID == 0 {
// ユーザーが存在しない
c.AbortWithStatus(http.StatusUnauthorized)
}
// ユーザー情報をコンテキストにセット
c.Set("user", user)
c.Next()
} else {
log.Fatal("jwtのパースに失敗しました")
}
}
検証
ログインメソッドの呼び出し。
また、ログインするとトークンがクッキーにセットされる(次の認証APIを参考)
認証メソッドの呼び出し。
パラメータには何も渡していないが、クッキーに保存されているJWTトークンをもとにユーザ情報を取得できていることが確認できる。
トークンの中身はこんな感じ
Discussion