はじめに
JWT(JSON Web Token)をSPAに使用するべきか否かの議論はありますが、今回はそれは度外視にGolangのEchoでJWTを使用するケースを想定し、実装をしてみようと思います。今回は説明を省きますが、当方フロントエンドにNext.js、AuthenticateにFirebaseを一部使用しています。またJWTの基本的な説明は公式サイトや他参考記事をご覧ください。
ゴール
- 任意のペイロードを設定できる
- GoアプリケーションでJWTが発行できる
- リクエスト内にJWTトークンを認可し適切な処理ができる
- JWTの送信者の本人認証ができる
実行環境
- Mac 11.5.2
- Docker Desktop 4.0.0
- Golang
- Echo
- Gorm
イメージ図
手順一覧
- Echo環境構築
- ルーティング設定
- ログインメソッドとペイロードの設定とJWTの発行
- 本人確認メソッド
- PostmanでのJWTの扱い方
- JavaScript(Typescript)でfetchメソッドでのJWTの取り扱い方
Echo環境構築
基本的なEchoの環境構築、実装は過去記事または公式をご参考ください。
ルーティング設定
EchoのJWT環境構築は公式を参考に構築していきます。
エンドポイント以降、"/restricted"をrとしてグループ化する。 上記でグループ化したrをEchoのmiddleware.JWTを経由させる。 こうすることで/restricted/以降はJWT認証を行う。
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
router := newRouter()
router.Logger.Fatal(router.Start(":8080"))
}
func newRouter() *echo.Echo {
e := echo.New()
e.Use(middleware.CORS())
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
r := e.Group("/restricted")
// グループ化されたrをmiddlwwareを経由するように設定
// ルーティングでeを選択するとJWT認証をせず、rを選択するとJWT認証を行うことができる。
r.Use(middleware.JWT([]byte("secret")))
r.POST("/login", middle_ware.Login())
r.POST("/user", controllers.UserCreate())
r.GET("/user/:id", controllers.UserShow())
r.PUT("/user/:id", controllers.UserEdit())
r.DELETE("/user/:id", controllers.UserDelete())
return e
}
ログインメソッドとJWTの発行
公式はHTMLのformタグから直接値を取得する方法で実装してますが、今回はNext.jsから送られてくるJSONを使用します。
type LoginUser struct {
Id string `json:"id"`
Email string `json:"email"`
Uid string `json:"uid"`
}
type AuthUser struct {
Id string `json:"id"`
Email string `json:"email"`
UidDigest string `json:"uid_digest"`
}
func Login() echo.HandlerFunc {
return func(c echo.Context) error {
db := dbconnect.Connect()
defer db.Close()
loginUser := new(LoginUser)
if err := c.Bind(loginUser); err != nil {
return err
}
email := loginUser.Email
uid := loginUser.Uid
user := AuthUser{}
result := db.Table("users").Find(&user, "email = ?", email)
if result.RecordNotFound() {
fmt.Println("IDかfirebase_idが間違っています")
return echo.ErrUnauthorized
} else {
hashedUid := user.UidDigest
err := bcrypt.CompareHashAndPassword([]byte(hashedUid), []byte(uid))
if err != nil {
fmt.Println("IDかfirebase_idが間違っています")
return echo.ErrUnauthorized
} else {
id := user.Id
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["uid"] = uid
claims["id"] = id
claims["admin"] = false
claims["iat"] = time.Now().Unix()
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
}
}
}
少し回りくどい実装をしているため解説いたします。
LoginUserを初期化し、JSONで送られてくるデータをバインドします。
バインドしたデータからメールアドレスとfirebase UIDを変数に格納します。
loginUser := new(LoginUser)
if err := c.Bind(loginUser); err != nil {
return err
}
email := loginUser.Email
uid := loginUser.Uid
取得したメースアドレスを基にusersテーブルからユーザーを取得します。
result := db.Table("users").Find(&user, "email = ?", email)
上記でユーザーが取得できた場合、ユーザー作成時にFIrebaes Uidをハッシュ化したUidDigestを基にログイン時に取得したFirebase UIDを比較します。
*ハッシュ化にはbcryptを使用しています。
hashedUid := user.UidDigest
err := bcrypt.CompareHashAndPassword([]byte(hashedUid), []byte(uid))
if err != nil {
fmt.Println("IDかfirebase_idが間違っています")
return echo.ErrUnauthorized
ペーロードの設定とTokenの発行
ここは各プロジェクトで必要な値を入れればOKです。
JWTはJavaScriptなどで容易にデコード可能なため、含める情報には機密情報は含めないことを強くお勧めいたします。
クレーム名 | 説明 |
---|---|
uid | Firebase UID |
id | usersテーブルのID |
admin | ユーザーを管理者かどうかを判断する true or false |
exp | JWT 有効期限 |
iat | isuued at の略。JWT 発行時刻 |
id := adminUser.Id
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["uid"] = uid
claims["id"] = id
claims["admin"] = true
claims["iat"] = time.Now().Unix()
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
ログインのメソッドを送ると以下のような結果が返ってきます。
{
"email": "golang.jwt@test.com",
"uid": "CS1z7P70T4ZYNkl0mg37Rujv6Iu1"
}
// 成功時
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTYzNzgyMzczNiwiaWF0IjoxNjM3NzM3MzM2LCJpZCI6InVzZXIiLCJ1aWQiOiJDUzF6N1A3MFQ0WllOa2wwbWczN1J1anY2SXUxIn0.bxHJChPl8Oi6Mfpub02k5POv0n5F8P4Z8kUilEeMN_E"
}
// 失敗時
{
"message": "Unauthorized"
}
本人確認メソッド
あくまでも一例ですが、パラメータとして送られてくるUserのIDと一緒に送られてくるJWTに含まれるidが一致するかをチェックするメソッドを紹介します。
rを含めるルーティング(EchoのMiddleware)はあくまでもJWTの有効性を確認するだけの処理なので本人確認までは担保しません。ですのでデータの編集や削除などの本人しか行わないような処理には本人確認処理を行うことをお勧めします。
// 本人かどうかでTrue/Falseを返す。
func Auth(c echo.Context, id string) bool {
currentUser := c.Get("user").(*jwt.Token)
claims := currentUser.Claims.(jwt.MapClaims)
userId := claims["id"].(string)
db := dbconnect.Connect()
defer db.Close()
user := User{}
// 渡されたuser_idでUserを取得する。
result := db.Table("users").Find(&user, "id = ?", id)
if result.RecordNotFound() {
fmt.Println("レコードが見つかりません")
return false
} else {
// 取得したUserのIDとJWTのIDが一致するかチェックする。
if user.Id != userId {
fmt.Println("ユーザーが一致しません")
return false
} else {
return true
}
}
}
func UserEdit() echo.HandlerFunc {
return func(c echo.Context) error {
db := dbconnect.Connect()
defer db.Close()
user_id := c.Param("id")
new_user := new(User)
user := User{}
// Authメソッドを呼び出し、パラメータのuser_idを渡す。
judgement := Auth(c, user_id)
if err := c.Bind(new_user); err != nil {
return err
}
if !judgement {
return c.JSON(http.StatusNotFound, nil)
} else {
result := db.Table("users").First(&user, "id = ?", user_id).Update(&new_user)
if result.RecordNotFound() {
fmt.Println("レコードが見つかりません")
return c.JSON(http.StatusNotFound, nil)
} else {
return c.JSON(http.StatusOK, user)
}
}
}
}
PostmanでのJWTの扱い方
- Authorizationをクリック
- Bearer Tokenを選択
- ログインメソッドで取得したJWTを入力する
あとはURLやJSONなどを含めてリクエストするだけです!
JavaScript(Typescript)でfetchメソッドでのJWTの取り扱い方
Typescriptですみません。型を消してくれれはOKです。
const editAdminUser = async (
data: Level,
id: string,
self_id: string,
jwt: string
) => {
try {
await fetch(
// 環境変数を呼び出してますが、エンドポイントが含まれます
process.env.NEXT_PUBLIC_API_URL_RESTRICTED +
"user/" +
"/" +
id,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer" + ":" + jwt,
},
body: JSON.stringify(data),
}
);
} catch (err) {
return err;
}
};
headerの中に以下を含めます。(jwtには実際のトークンを代入してください)
Authorization: "Bearer" + ":" + jwt
よかったら参考にしてみてください。
それでは👋