Echo+JWTでAPIサーバーを実装する
はじめに
今回、Echo と JWT を使用したプログラムを趣味で作成する機会があったので、JWT に関連する部分を切り取って記事にしました。
今回、記事で示すサンプルプログラムは以下のプログラム中から関係のある部分を切り取って少し記事に合わせて修正したものです。興味があれば、ぜひ確認してみてください。
環境
# 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を使用しました。
実装
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 サーバーの初期化・ミドルウェアの設定を行っています。
気をつけないといけないこと
-
JWT を生成する際に使用する
jwt.NewWithClaims()
の第一引数に渡される署名に使用されるアルゴリズムは、ミドルウェアを初期化する際に指定する署名アルゴリズムと一致している必要がある。 -
署名に使用する秘密鍵は、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) }, },
-
JWT ミドルウェアは、本人確認までは行わない(あくまでも JWT の有効性を確認するのみ)なので、期限の確認などは手動で実装する必要がある。
JWT は Base64 で変換されているため、簡単に復号できるため機密情報を含めてはいけない
最後に
Echo と JWT を使用した認証の実装を行いました。今回の記事中のプログラムでは、認証まで実装していませんが Go で Echo+JWT の実装を行おうと思ってる人の参考になったなら幸いです。
参考資料
Discussion