こんにちは、KOUKIです。
GolangのWebフレームワークであるfiberを使ってAPIを開発しています。
前回は、Ambassador APIのマルチルート設定を行いました。
今回は、リクエストスコープの設定を行います。
尚、この記事に出てくるソースコードは、Udemyの「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
<目次>
前回
作るもの
Ambassdor機能を作りたいと思います。エンドポイントは、次の通りです。
- POST /api/ambassador/register
- POST /api/ambassador/login
- GET /api/ambassador/user
- POST /api/ambassador/logout
- PUT /api/ambassador/users/info
- PUT /api/ambassador/users/password
リクエストスコープの設定
本APIには、Ambassador APIとAdmin APIがあり、それぞれのパスにリクエストを送ります。
前回、それを可能にするために、Ambassador用のマルチルートを設定しました。
しかし、ログインする際に一つ問題があります。
ログインする時は、JWTトークンを作成し、それをCookieに保存していました。その情報は、Ambassdor/Adminどちらの情報のものなのか区別をつけていないので、片方のAPIでログインしてしまうともう片方のAPIが処理できてしまうというバグがあります。
これを是正するために、リクエストの種類を判別するスコープを設けたいと思います。
JWTトークン処理の移行
最初に、auathController.goのログイン関数に実装していたJWTトークンの作成処理をMiddleware関数として外出しします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// auth.go package middleware import ( "strconv" "time" "github.com/dgrijalva/jwt-go" "github.com/gofiber/fiber/v2" ) ... func GenerateJWT(id uint) (string, error) { // トークンの発行 payload := jwt.StandardClaims{ Subject: strconv.Itoa(int(id)), ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), } return jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString([]byte("secret")) } |
この変更により、ログイン処理を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// controllers/auathController.go ... func Login(ctx *fiber.Ctx) error { ... // パスワードチェック ... token, err := middleware.GenerateJWT(user.ID) // 追加 if err != nil { ... } // Cookieに保存 ... } |
リクエストスコープの設定
次に、リクエストスコープを設定します。
Middlewareのauth.goに、以下の構造体を宣言しましょう。
1 2 3 4 5 6 7 |
// auth.go const SecretKey = "secret" type ClaimsWithScope struct { jwt.StandardClaims Scope string } |
Scopeプロパティには、”admin”などの文字列を入れて、リクエストを判別します。
次に、auth.goのIsAuthenticate関数を修正します。
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 |
// auth.go func IsAuthenticate(ctx *fiber.Ctx) error { // cookieから情報を取得 ... // token取得 ... if err != nil || !token.Valid { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証がされていません", }) } payload := token.Claims.(*ClaimsWithScope) IsAmbassador := strings.Contains(ctx.Path(), "/api/ambassador") if (payload.Scope == "admin" && IsAmbassador) || (payload.Scope == "ambassador" && !IsAmbassador) { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証が許可されません", }) } return ctx.Next() } |
次に、GenerateJWT関数を修正します。
1 2 3 4 5 6 7 8 9 10 |
// auth.go func GenerateJWT(id uint, scope string) (string, error) { // トークンの発行 payload := ClaimsWithScope{} payload.Subject = strconv.Itoa(int(id)) payload.ExpiresAt = time.Now().Add(time.Hour * 24).Unix() payload.Scope = scope return jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString([]byte(SecretKey)) } |
GetUser関数も修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// auth.go func GetUserID(ctx *fiber.Ctx) (uint, error) { // cookieから情報を取得 cookie := ctx.Cookies("jwt") // token取得 token, err := jwt.ParseWithClaims( cookie, &ClaimsWithScope{}, func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil }, ) if err != nil { return 0, err } payload := token.Claims.(*ClaimsWithScope) id, _ := strconv.Atoi(payload.Subject) return uint(id), nil } |
この修正に伴い、authControllers.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 |
func Login(ctx *fiber.Ctx) error { ... // パスワードチェック ... // ambassador判定 isAmbassador := strings.Contains(ctx.Path(), "/api/ambassador") var scope string if isAmbassador { scope = "ambassador" } else { scope = "admin" } if !isAmbassador && user.IsAmbassador { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証が許可されていません", }) } token, err := middleware.GenerateJWT(user.ID, scope) if err != nil {...} // Cookieに保存 ... } |
検証
検証してみましょう。
まずは、admin APIでログインします。
- URL: http://localhost:8000/api/admin/login
- 形式: POST
1 2 3 4 |
{ "email": "self@ne.jp", "password": "b" } |

次に、admin APIのuserを実行します。
- URL: http://localhost:8000/api/admin/user
- 形式: GET

次に、この状態でAmbassador API側のuserを実行します。
- URL: http://localhost:8000/api/ambassador/user
- 形式: POST

「認証がされていません」と出ましたね! 成功です。
一応、Ambassador側でログインしてから、もう一度userを叩いてみましょう。
- URL: http://localhost:8000/api/ambassador/login
- 形式: GET
1 2 3 4 |
{ "email": "ambassdor@ne.jp", "password": "b" } |

この状態で、もう一度Ambassadorのuserを叩きます。

OKですね。
APIごとにリクエストが振り分けられていることが確認できたと思います。
次回
次回は、Ailasesを実装しましょう。
Go言語まとめ
ソースコード
ここまでのソースコードを以下に記載します。
auth.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 |
// auth.go package middleware import ( "fmt" "strconv" "strings" "time" "github.com/dgrijalva/jwt-go" "github.com/gofiber/fiber/v2" ) const SecretKey = "secret" type ClaimsWithScope struct { jwt.StandardClaims Scope string } func IsAuthenticate(ctx *fiber.Ctx) error { // cookieから情報を取得 cookie := ctx.Cookies("jwt") // token取得 token, err := jwt.ParseWithClaims( cookie, &ClaimsWithScope{}, func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil }, ) if err != nil || !token.Valid { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証がされていません", }) } payload := token.Claims.(*ClaimsWithScope) IsAmbassador := strings.Contains(ctx.Path(), "/api/ambassador") if (payload.Scope == "admin" && IsAmbassador) || (payload.Scope == "ambassador" && !IsAmbassador) { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証が許可されません", }) } return ctx.Next() } func GenerateJWT(id uint, scope string) (string, error) { // トークンの発行 payload := ClaimsWithScope{} payload.Subject = strconv.Itoa(int(id)) payload.ExpiresAt = time.Now().Add(time.Hour * 24).Unix() payload.Scope = scope return jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString([]byte(SecretKey)) } func GetUserID(ctx *fiber.Ctx) (uint, error) { // cookieから情報を取得 cookie := ctx.Cookies("jwt") // token取得 token, err := jwt.ParseWithClaims( cookie, &ClaimsWithScope{}, func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil }, ) if err != nil { fmt.Println(err) return 0, err } payload := token.Claims.(*ClaimsWithScope) id, _ := strconv.Atoi(payload.Subject) return uint(id), nil } |
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
// controllers/auathController.go package controllers import ( "admin/src/database" "admin/src/middleware" "admin/src/models" "strings" "time" "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, IsAmbassador: strings.Contains(ctx.Path(), "/api/ambassador"), } // パスワードセット user.SetPassword(data["password"]) // ユーザー作成 result := database.DB.Create(&user) if result.Error != nil { ctx.Status(fiber.StatusBadRequest) return ctx.JSON(fiber.Map{ "message": "そのEmailは既に登録されています", }) } 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": "ログイン情報に誤りがあります", }) } // ambassador判定 isAmbassador := strings.Contains(ctx.Path(), "/api/ambassador") var scope string if isAmbassador { scope = "ambassador" } else { scope = "admin" } if !isAmbassador && user.IsAmbassador { ctx.Status(fiber.StatusUnauthorized) // 401 return ctx.JSON(fiber.Map{ "message": "認証が許可されていません", }) } token, err := middleware.GenerateJWT(user.ID, scope) 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", }) } func User(ctx *fiber.Ctx) error { id, _ := middleware.GetUserID(ctx) // ユーザー検索 var user models.User database.DB.Where("id = ?", id).First(&user) return ctx.JSON(user) } func Logout(ctx *fiber.Ctx) error { // cookieをクリアする cookie := fiber.Cookie{ Name: "jwt", Value: "", Expires: time.Now().Add(-time.Hour * 24), // -を指定 HTTPOnly: true, } ctx.Cookie(&cookie) return ctx.JSON(fiber.Map{ "message": "success", }) } func UpdateInfo(ctx *fiber.Ctx) error { var data map[string]string // リクエストデータをパースする if err := ctx.BodyParser(&data); err != nil { return err } // cookieからidを取得する id, _ := middleware.GetUserID(ctx) user := models.User{ FirstName: data["first_name"], LastName: data["last_name"], Email: data["email"], } user.ID = id // ユーザー情報更新 database.DB.Model(&user).Updates(&user) return ctx.JSON(user) } func UpdatePassword(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": "パスワードに誤りがあります", }) } // cookieからidを取得する id, _ := middleware.GetUserID(ctx) user := models.User{} user.ID = id // パスワードセット user.SetPassword(data["password"]) // ユーザー情報更新 database.DB.Model(&user).Updates(&user) return ctx.JSON(user) } |
コメントを残す
コメントを投稿するにはログインしてください。