LoginSignup
3
11

More than 1 year has passed since last update.

Golang + EchoでJWTを使ってみる

Last updated at Posted at 2021-11-25

はじめに

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

イメージ図

test.png

手順一覧

  • Echo環境構築
  • ルーティング設定
  • ログインメソッドとペイロードの設定とJWTの発行
  • 本人確認メソッド
  • PostmanでのJWTの扱い方
  • JavaScript(Typescript)でfetchメソッドでのJWTの取り扱い方

Echo環境構築

基本的なEchoの環境構築、実装は過去記事または公式をご参考ください。

ルーティング設定

EchoのJWT環境構築は公式を参考に構築していきます。

エンドポイント以降、"/restricted"をrとしてグループ化する。 上記でグループ化したrをEchoのmiddleware.JWTを経由させる。 こうすることで/restricted/以降はJWT認証を行う。

main.go
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の扱い方

  1. Authorizationをクリック

スクリーンショット 2021-11-25 17.06.54.png

  1. Bearer Tokenを選択

スクリーンショット 2021-11-25 17.07.49.png

  1. ログインメソッドで取得したJWTを入力する

スクリーンショット 2021-11-25 17.08.54.png

あとは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

よかったら参考にしてみてください。
それでは👋

参考記事

3
11
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
3
11