こんにちは、KOUKIです。
前回は、ユーザーの登録とログインの処理を実装しました。
今回は、JWTを使って、トークンの発行処理とプラスαを実装したいと思います。
尚、この記事に出てくるソースコードは、Udemyの「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
<目次>
前回
事前準備
モジュールのインストール
1 |
go get github.com/dgrijalva/jwt-go |
作るもの
認証機能を作りたいと思います。エンドポイントは、次の通りです。
- POST /api/admin/register
- POST /api/admin/login
- POST /api/admin/logout
- GET /api/admin/user
- PUT /api/admin/users/info
- PUT /api/admin/users/password
今回は、ログイン処理(login)にトークン発行機能を追加します。
トークンが必要な理由
最初に、トークンが必要な理由を軽く触れておきます。トークンは、ログインが成功したら発行します。
もしこのトークンを発行しない場合、APIへアクセスするたびにいちいちログインして認証をPASSしなければなりません。それは、あまりに不便です。
それよりも、ログイン成功後に有効期限付きのトークンを発行し、それ以降のAPIとのやりとりはトークンが有効の間だけOKにする、といった実装にすれば効率的かつシンプルな実装になります。
トークンの発行は、JWTを使うと割と簡単に実装できるので、おすすめです。
ログイン処理の修正
トークンを発行するタイミングはログイン後なので、ログイン処理に機能を追加します。
JWTトークン発行
前回実装したauathController.goのLogin関数に、トークン発行処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func Login(ctx *fiber.Ctx) error { ... // パスワードチェック ... // トークンの発行 payload := jwt.StandardClaims{ Subject: strconv.Itoa(int(user.ID)), ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), } token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString([]byte("secret")) if err != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } return ctx.JSON(token) } |
検証
APIヘ以下のパラメータでリクエストを送り、トークンが発行されるか確認しましょう。
- http://localhost:8000/api/admin/login
- POST
1 2 3 4 |
{ "email": "self@ne.jp", "password": "a" } |

1 |
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjMwNTE1OTEsInN1YiI6IjEifQ.JMgHxzXDWg8QYg1PpluR-Jm1Nx04gpFt-i4vRxx-wxE" |
OKですね。問題なくトークンが取得できました。
Cookie
発行したトークンは、Cookieに保存します。fiberには、Cookieメソッドがあるので、簡単に実装できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func Login(ctx *fiber.Ctx) error { ... // トークンの発行 ... // Cookieに保存 cookie := fiber.Cookie{ Name: "jwt", Value: token, Expires: time.Now().Add(time.Hour * 24), HTTPOnly: true, } ctx.Cookie(&cookie) return ctx.JSON(fiber.Map{ "message": "success", }) } |
これで、Cookieに「Key: jwt, Value: token」の情報が保存されるようになります。
CORSの設定
CORS設定のAllowCredentialsを利用して、認証する際にCookie情報を参照するか否かを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package main import ( "admin/src/database" "admin/src/routes" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" ) func main() { ... // fiber API ... // CORSの設定 app.Use(cors.New(cors.Config{ // 認証にcookieなどの情報を必要とするかどうか AllowCredentials: true, })) ... } |
リファクタリング
JWTトークンの実装は完了したのですが、リファクタリングしたい箇所が出てきたので、以下に記述します。
パスワードセット
パスワードを設定する専用のメソッドをUser構造体に作成します。
1 2 3 4 5 6 7 8 9 10 11 |
// models/user.go ... type User struct { ... } func (u *User) SetPassword(pwd string) { // ハッシュパスワードを作成 hashPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), 12) u.Password = hashPwd } |
このメソッドは、Register関数から呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// controllers/auathController.go ... func Register(ctx *fiber.Ctx) error { ... // ハッシュパスワードを作成 ... // パスワードセット user.SetPassword(data["password"]) // ユーザー作成 ... } |
パスワードチェック
パスワードチェック処理もUser構造体にメソッドとして実装します。
1 2 3 4 5 6 7 8 9 10 11 |
// models/user.go .... func (u *User) SetPassword(pwd string) { ... } func (u *User) ComparePassword(pwd string) error { return bcrypt.CompareHashAndPassword(u.Password, []byte(pwd)) } |
この関数は、Login関数から呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func Login(ctx *fiber.Ctx) error { ... // パスワードチェック err := user.ComparePassword(data["password"]) <<<< ここ if err != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // トークンの発行 payload := jwt.StandardClaims{ .... // Cookieに保存 .... } |
次回
次回は、ユーザー認証まわりとログアウトの処理を実装します。
Go言語まとめ
ソースコード
ここまでのソースコードを以下に記載します。
models.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// models/user.go package models import "golang.org/x/crypto/bcrypt" type User struct { ID uint FirstName string LastName string Email string Password []byte IsAmbassdor bool } func (u *User) SetPassword(pwd string) { // ハッシュパスワードを作成 hashPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), 12) u.Password = hashPwd } func (u *User) ComparePassword(pwd string) error { return bcrypt.CompareHashAndPassword(u.Password, []byte(pwd)) } |
auathController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
// controllers/auathController.go package controllers import ( "admin/src/database" "admin/src/models" "strconv" "time" "github.com/dgrijalva/jwt-go" "github.com/gofiber/fiber/v2" "golang.org/x/crypto/bcrypt" ) func Register(ctx *fiber.Ctx) error { var data map[string]string if err := ctx.BodyParser(&data); err != nil { return err } if data["password"] != data["password_confirm"] { ctx.Status(fiber.StatusBadRequest) // 400 return ctx.JSON(fiber.Map{ "message": "パスワードに誤りがあります", }) } // ハッシュパスワードを作成 pwd, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 12) user := models.User{ FirstName: data["first_name"], LastName: data["last_name"], Email: data["email"], Password: pwd, IsAmbassdor: false, } // パスワードセット user.SetPassword(data["password"]) // ユーザー作成 database.DB.Create(&user) return ctx.JSON(user) } func Login(ctx *fiber.Ctx) error { var data map[string]string if err := ctx.BodyParser(&data); err != nil { return err } var user models.User database.DB.Where("email = ?", data["email"]).First(&user) if user.ID == 0 { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // パスワードチェック err := user.ComparePassword(data["password"]) if err != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // トークンの発行 payload := jwt.StandardClaims{ Subject: strconv.Itoa(int(user.ID)), ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), } token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString([]byte("secret")) if err != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "ログイン情報に誤りがあります", }) } // Cookieに保存 cookie := fiber.Cookie{ Name: "jwt", Value: token, Expires: time.Now().Add(time.Hour * 24), HTTPOnly: true, } ctx.Cookie(&cookie) return ctx.JSON(fiber.Map{ "message": "success", }) } |
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main import ( "admin/src/database" "admin/src/routes" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" ) func main() { // Connection To Mysql database.Connect() // Migration database.AutoMigrate() // fiber API app := fiber.New() // CORSの設定 app.Use(cors.New(cors.Config{ // 認証にcookieなどの情報を必要とするかどうか AllowCredentials: true, })) // Setup Routes routes.Setup(app) app.Listen(":3000") } |
コメントを残す
コメントを投稿するにはログインしてください。