Echo+JWTでAPIサーバーを実装する

2023/12/25に公開

はじめに

今回、Echo と JWT を使用したプログラムを趣味で作成する機会があったので、JWT に関連する部分を切り取って記事にしました。
今回、記事で示すサンプルプログラムは以下のプログラム中から関係のある部分を切り取って少し記事に合わせて修正したものです。興味があれば、ぜひ確認してみてください。

https://github.com/aqyuki/go-api

環境

# OS : windows10 + WSL2 + zsh

$ go version
go version go1.21.5 linux/amd64

ライブラリの導入

まずは、必要なライブラリ等を追加します。

go get github.com/labstack/echo@latest
go get github.com/labstach/echo-jwt@latest
go get github.com/golang-jwt/jwt@latest

echo … Go で API サーバーを作成するためのフレームワーク

echo-jwt …  Echo で使用することができる JWT ミドルウェア(echo のミドルウェアは非推奨なため別途導入)

golang-jwt/jwt …  JWT をカスタムして作成するために必要。

JWT を使用するに当たって Echo の JWT ミドルウェアは非推奨となっていたため、公式ドキュメントを参考にecho-jwtを使用しました。

実装

server.go
package server

import (
	"net/http"
	"time"

	"github.com/golang-jwt/jwt/v5"
	echojwt "github.com/labstack/echo-jwt/v4"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

// リクエストボディー
type CreateRequest struct {
	ID string `json:"id"`
}

// JWTのClaims
type accountClaims struct {
	ID string `json:"id"`
	jwt.RegisteredClaims
}

// Server
type Server struct {
	e *echo.Echo
}

func (s *Server) Create(c echo.Context) error {
	// リクエストからIDを取得
	req := new(CreateRequest)
	if err := c.Bind(req); err != nil {
		return c.String(http.StatusBadRequest, "invalid request")
	}

	// JWTを作成するためのClaimsを作成
	claims := &accountClaims{
		ID: req.ID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 1)),
		},
	}

	// JWTを作成
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	t, err := token.SignedString([]byte("secret")) // 署名
	if err != nil {
		return c.String(http.StatusInternalServerError, "failed to sign token")
	}
	return c.JSON(http.StatusOK, map[string]string{"token": t})
}

func (s *Server) Check(c echo.Context) error {

	// JWTの認証を通過した場合、ユーザー情報がc.Get("user")に格納される
  // デフォルトでは、 c.Get("user")に保存されているが、ミドルウェアの設定で指定することも可能
	token, ok := c.Get("user").(*jwt.Token)
	if !ok {
		return c.String(http.StatusUnauthorized, "invalid token")
	}

	// echo.Contextから取り出したtokenをパースする
	claims, ok := token.Claims.(*accountClaims)
	if !ok {
		return c.String(http.StatusUnauthorized, "invalid token")
	}

	return c.JSON(http.StatusOK, map[string]string{"id": claims.ID})
}

func New() *Server {
	e := echo.New()

	// Middleware
	e.Use(middleware.Recover())
	e.Use(middleware.Logger())

	// グループ化 : グループ単位でミドルウェアを変更する
	// 今回はprivate groupにJWTの認証を追加する
	publicGroup := e.Group("/public")
	privateGroup := e.Group("/private")

	// JWTミドルウェアを設定
	privateGroup.Use(echojwt.WithConfig(
		echojwt.Config{
			SigningKey: []byte("secret"), // 署名に使用する鍵
			NewClaimsFunc: func(c echo.Context) jwt.Claims {
        // echo.Contextから取り出したtokenを格納するキー
        // この値を変更することで、変えることができる。
        // ContextKey: 	"user",

				// JWTをClaimsに変換する際に呼び出される
				return new(accountClaims)
			},
		},
	))

	s := &Server{e: e}
	publicGroup.POST("/create", s.Create)
	privateGroup.GET("/check", s.Check)
	return s
}

  • s.Create

    /public/createに POST リクエストを送ると動作するハンドラー。内部では、リクエストボディーから ID を取得して JWT を発行しています。

  • s.Check

    /private/checkに GET リクエストを送ると動作するハンドラー。内部では、トークンから生成された構造体を使用して、レスポンスを組み立てています。

  • New

    echo サーバーの初期化・ミドルウェアの設定を行っています。

気をつけないといけないこと

  1. JWT を生成する際に使用するjwt.NewWithClaims()の第一引数に渡される署名に使用されるアルゴリズムは、ミドルウェアを初期化する際に指定する署名アルゴリズムと一致している必要がある。

  2. 署名に使用する秘密鍵は、JWT 生成時と検証時で一致している必要がある。

    生成時のtoken.SignedString()の第一引数とechojwt.Config.SigningKeyが一致している必要がある

    t, err := token.SignedString([]byte("secret")) // 署名
    
    echojwt.Config{
    			SigningKey: []byte("secret"), // 署名に使用する鍵
    			NewClaimsFunc: func(c echo.Context) jwt.Claims {
    				// JWTをClaimsに変換する際に呼び出される
    				return new(accountClaims)
    			},
    		},
    
  3. JWT ミドルウェアは、本人確認までは行わない(あくまでも JWT の有効性を確認するのみ)なので、期限の確認などは手動で実装する必要がある。

JWT は Base64 で変換されているため、簡単に復号できるため機密情報を含めてはいけない

最後に

Echo と JWT を使用した認証の実装を行いました。今回の記事中のプログラムでは、認証まで実装していませんが Go で Echo+JWT の実装を行おうと思ってる人の参考になったなら幸いです。

参考資料

https://qiita.com/gs1068/items/7dc95a21eb6611714855

https://echo.labstack.com/docs/cookbook/jwt

https://github.com/labstack/echo-jwt

https://zenn.dev/mikakane/articles/tutorial_for_jwt

Discussion