12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ginを利用して新規登録、ログイン機能をJWT認証で実装(JWT認証編)

Posted at

概要

新規登録からログインまで解説してきて、いよいよ今回でJWT認証の実装を行なっていきます。

正直あまり参考になる記事がなく、自分なりにパッケージのドキュメントや海外の記事を参考にしました。

間違っている点もあるかもしれませんが、少しでも参考になればと思いアウトプットしていきます。

また、JWT認証についてよくわからない方は下記の記事で解説していますのでそちらを参照してください。

過去のシリーズ

環境

過去の記事にDockerの設定については記載しているのでそちらを参考にしてください。

なお、認証機能を構築するのにjwt-goパッケージを利用していきます。

バージョンはv5.0.0-rc.1を使用します。

JWT認証実装

今回は共通鍵認証を前提とします。

処理を記載する前にまずは、pkgディレクトリ配下にtoken.goを作成してください。

作成したファイルに処理を記載していきます。

JWT生成

まずはJWTを生成する関数を作成していきます。

これは新規登録時ログイン時にクライアントにレスポンスとしてトークンを返すために作成します。

pkg/utils/token.go
package utils

import (
	"os"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func GenerateToken(userId uint) (string, error) {
	secretKey := os.Getenv("SECRET_KEY") // 暗号化、復号化するためのキー
	tokenLifeTime, err := strconv.Atoi(os.Getenv("TOKEN_LIFETIME"))
	if err != nil {
		return "", err
	}

	claims := jwt.MapClaims{
		"user_id": userId,
		"exp":     time.Now().Add(time.Hour * time.Duration(tokenLifeTime)).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(secretKey))
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

claimsにはuser_idを設定しました。

調べた中でPasswordemailclaimに保存している記事がありましたが、

個人的にはJWTに個人情報は含めるべきではないと考えているので避けました。

なぜ個人情報を含めるべきではないというと、

JWTはあくまでBase64Urlというアルゴリズムでエンコードしたにすぎません。

もしJWTが流失してしまった場合に、デコードされるとJWTの中身が分かってしまします。

なので個人情報は含めるべきではないと考え、今回はuser_idを格納しました!

secretKeyは署名を暗号化・復号化するために必要です。

基本的に文字列ならなんでもいいですが、

私は秘密鍵などを生成する際によく使用されるopensslコマンドを利用して作成しました。

openssl rand -base64 32

これはBASE64形式のランダムなデータを32バイト長で生成してくれます。

おそらく上記のコマンドを実行したら下記のような文字列がターミナルに出力されたのではないでしょうか。

0N3mCELJP4RvrKyigmq/Q0uB+ak3TGDuBRSSRKQvv9s=

ここで出力されたを値を各自設定してください。

また、トークンの有効期限も設定します。

今回TOKEN_LIFETIMEという環境変数で設定していますが、各自好きなように設定してください。

トークンの有効期限のできるだけ短くしたほうが良いとされています。

なぜならトークンは一度発行されると削除することが困難なのため、数カ月単位などの長期期間は避けたほうがいいです。

新規登録処理を編集

前段ではトークンを生成する関数を作成しました。

今度は作成した関数をcontrollersの新規登録ハンドラーの中で呼び出したいので以前の記事で作成した新規登録処理部分を編集していきます。

またクライアントとのトークンをやりとりはCookieを利用します。

/pkg/models/auth.go
func (handler *Handler) SignUpHandler(ctx *gin.Context) {
	var signUpInput models.SignUpInput
	err = ctx.ShouldBind(&signUpInput)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": "Invalid request body",
		})
		return
	}

	user := &models.User{
		Name:     signUpInput.Name,
		Email:    signUpInput.Email,
		Password: signUpInput.Password,
	}
	err = user.Validate()
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": err.Error(),
		})
		return
	}

	newUser, err := user.Create(handler.DB)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": "Failed to create user",
		})
		return
	}

    // 追加部分
	token, err := utils.GenerateToken(newUser.ID)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": "Failed to sign up",
		})
		return
	}

    // Cookieにトークンをセット
	ctx.SetCookie("token", token, cookieMaxAge, "/", "localhost", false, true)
	ctx.JSON(http.StatusOK, gin.H{
		"user_id": newUser.ID,
		"message": "Successfully created user",
	})
}

先ほど作成したGenerateTokenでトークンを生成し、

生成したトークンをSetCookieCookieにセットしています。

SetCookieメソッドについて各パラメータが何を指しているのか知りたい方は下記を参照してください。

Ginのソースコードを見ても各パラメーターが何を指しているのかよくわからなかったので下記を参考にしました!

JWTを検証する関数を生成

次にリクエストで送られてきたJWTを検証する関数を作成していきます。

pkg/utils/token.go
package utils

import (
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func GenerateToken(userId uint) (string, error) {
	secretKey := os.Getenv("SECRET_KEY")
	tokenLifeTime, err := strconv.Atoi(os.Getenv("TOKEN_LIFETIME"))
	if err != nil {
		return "", err
	}

	claims := jwt.MapClaims{
		"user_id": userId,
		"exp":     time.Now().Add(time.Hour * time.Duration(tokenLifeTime)).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(secretKey))
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

// 下記追加部分
func ParseToken(tokenString string) (*jwt.Token, error) {
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("SECRET_KEY")), nil
	})
	if err != nil {
		return nil, err
	}
	return token, nil
}

jwt.Parseでトークンを検証し、不正なトークンの場合にエラーを返すようになっています。

ミドルウェアを作成

次にmiddlewareを作成していきます。

まず、pkgディレクトリ配下にmiddlewareディレクトリを作成し、middleware.goファイルを作ります。

具体的な処理は下記になります。

pkg/middleware/middleware.go
package middleware

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"app/pkg/utils"
)

func AuthMiddleware(ctx *gin.Context) {
	tokenString, err := ctx.Cookie("token")
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, gin.H{
			"message": "Unauthorized",
		})
		ctx.Abort()
		return
	}

	token, err := utils.ParseToken(tokenString)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, gin.H{
			"message": "Invalid token",
		})
		ctx.Abort()
		return
	}

	ctx.Next()
}

処理の流れとしては下記です。

  1. リクエスト内のクッキーからJWTを取得
  2. 取得したJWTを検証する
  3. そして処理をHandlerに投げます

またトークンなどに不正があり認証できない場合は、後続のハンドラーに処理がいかないように

ctx.Abort()で処理をストップさせなければなりません。

これがないと、後続のハンドラーの処理が続いてしまうので必須です。

ミドルウェアの設定

あとは認証が必要なエンドポイントごと設定すればmiddlewareを使用できます。

あくまで例ですが、

下記のようにmiddlewareを利用します。

router.go
package router

import (
	"github.com/gin-gonic/gin"

	"app/pkg/controllers"
	"app/pkg/database"
    "app/pkg/middleware"
)

func Run() {
	router := setupRouter()
	router.Run()
}

func setupRouter() *gin.Engine {
	router := gin.Default()
	handler := &controllers.Handler{
		DB: database.GetDB(),
	}
	api := router.Group("/api")
	v1 := api.Group("/v1")
    users := v1.Group("/users")
    users.Use(middleware.AuthMiddleware) // middlewareを設定
    {
        users.GET("", handler.GetUsers) // ユーザー一覧を取得
    }

	return router
}

users.Use(middleware.AuthMiddleware)の部分でmiddlewareを設定しています。

上記設定であれば、users配下のエンドポイントにアクセスする際はmiddlewareが作動します。

なので今後認証した場合のみエンドポイントへのアクセスを許可する場合は、上記のように追加していけばOKです。

まとめ

いかがだったでしょうか。

新規登録〜ログイン〜JWT認証の3回に渡って解説してきました。

正直まだまだ勉強中なので未熟な部分もあるかと思います。

その場合は遠慮なくコメントいただけるとありがたいです!

なお、今回はログの管理やトークンからユーザーIDを抽出するような処理は記載しませんでしたが、

本来はあったほうがいいと思います。

実装する際はその辺も考えながら実装するといいかもしれません。

最後まで読んでいただいてありがとうございました!

参考

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?