Ginでgin-jwtを使って簡単にAPIに認証機能を実現する
はじめに
前回のエントリ で、API に JWT 認証の仕組みを実現してみました。
それなりのコード量となりましたが、JWT 認証に関わらず認証は多くのサービスで必須といっても良い機能です。
すでにライブラリがありそうだと思って探すとやはりありました。
こちらを使って JWT 認証機能を実現してみます。
gin-jwt ( JWT Middleware for Gin Framework ) とは
前回使ったモジュールである jwt-go を内部で利用しています。
Gin フレームワークの Middleware 機能を利用しており、簡単に導入できます。
また、コントローラロジックと JWT のチェックロジックを切り離して見通しを良くできます。
リクエストフィルタとして JWT のチェックを行うだけでなく、 ログイン や トークンのリフレッシュ のためのコントローラ関数も組み込みで用意されており、機能が豊富です。
gin-jwt を使って JWT 認証を実装してみる
いつものように使ってみます。
プロジェクトの作成
プロジェクトフォルダの作成、移動、モジュールの初期化を行います。
その後 git-jwt の version2 をインストールします。
$ mkdir gin-jwt
$ cd gin-jwt
$ go mod init genzouw/gin-jwt
go: creating new go.mod: module genzouw/gin-jwt
$ go get github.com/appleboy/gin-jwt/v2
main.go
実装
main.go
を作成します。
$ vi main.go
今回提供する API エンドポイントは以下になります。
2つ目、3つ目の API は認証必須であり、TOKEN が必要です。
/api/login
: 指定された "email" 、"password" が正しければ TOKEN を取得/api/refresh_token
: [ 認証必須 ] 有効期限を延長した TOKEN を取得/api/users/me
: [ 認証必須 ] ログイン中ユーザー情報を取得
コードは以下のようになります。
JWT 認証がシンプルに実装されていることがわかります。
簡単なコードの解説をしておきます。
newJwtMiddleware()
関数で Middleware の実体を取得- Gin のルーティングに Middleware を設定
- その他、存在しないパスにリクエスされた場合を
router.NoRoute()
ルーティングで拾い、404 レスポンスを返す
JWT の設定(Realm 名、Secret Key、有効期限)などは newJwtMiddleware()
関数に押し込んでいすが必要があれば設定変更が可能です。
package main
import (
"log"
"net/http"
"time"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
func main() {
jwtMiddleware, err := newJwtMiddleware()
if err != nil {
log.Fatal(err)
return
}
r := gin.Default()
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})
api := r.Group("/api")
{
api.POST("/login", jwtMiddleware.LoginHandler)
api.GET("/refresh_token", jwtMiddleware.RefreshHandler)
me := api.Group("/users/me").Use(jwtMiddleware.MiddlewareFunc())
{
me.GET("", func(c *gin.Context) {
userID := userIdInJwt(c)
// TODO : 一般的にはデータベースやストレージ、SaaSからuserIDを元にユーザー情報を取得する
c.JSON(http.StatusOK, gin.H{
"userID": userID,
})
})
}
}
port := "8000"
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
}
}
func userIdInJwt(c *gin.Context) string {
claims := jwt.ExtractClaims(c)
userID := claims[jwt.IdentityKey]
return userID.(string)
}
func newJwtMiddleware() (*jwt.GinJWTMiddleware, error) {
jwtMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour * 24,
MaxRefresh: time.Hour * 24 * 7,
SendCookie: false,
PayloadFunc: func(data interface{}) jwt.MapClaims {
return jwt.MapClaims{
jwt.IdentityKey: data,
}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var l loginRequest
if err := c.ShouldBind(&l); err != nil {
return "", jwt.ErrMissingLoginValues
}
if !l.isValid() {
return "", jwt.ErrFailedAuthentication
}
return l.Email, nil
},
})
if err != nil {
return nil, err
}
err = jwtMiddleware.MiddlewareInit()
if err != nil {
return nil, err
}
return jwtMiddleware, nil
}
type loginRequest struct {
Email string `form:"email" json:"email" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func (l loginRequest) isValid() bool {
// TODO : 一般的にはデータベースやストレージ、SaaSから取得する
passwords := map[string]string{
"admin@gmail.com": "admin",
"test@gmail.com": "test",
}
return passwords[l.Email] == l.Password
}
驚くほどシンプルに実装できました。
注意
今回はユーザーの登録機能は実装していません。
ユーザー情報もコード上にベタがきされた admin@mail.com
、test@gmail.com
ユーザーしか扱っておらず、このいずれかのユーザーでしかログインに利用できない点に注意が必要です 。
多くの場合、データベースに化膿されているユーザー情報と突き合わせすることになるでしょう 。
動作確認
main.go
を起動します。
$ go run main.go
今までは動作確認のためにcurl
コマンドを使っていましたが、最近は http
コマンドを使っています。
今回もhttp
コマンドを使おうと思います。
もちろんcurl
コマンドでも問題ありませんし、ブラウザでも構いません。
異常ケースから試してみます。
# 未認証の状態でのアクセス
$ http --json GET localhost:8000/api/refresh_token
HTTP/1.1 401 Unauthorized
Content-Length: 45
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:45:09 GMT
Www-Authenticate: JWT realm=test zone
{
"code": 401,
"message": "auth header is empty"
}
# 未認証の状態でのアクセス
$ http --json GET localhost:8000/api/users/me
HTTP/1.1 401 Unauthorized
Content-Length: 45
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:45:21 GMT
Www-Authenticate: JWT realm=test zone
{
"code": 401,
"message": "auth header is empty"
}
# 存在しないパスへのアクセス
$ http --json GET localhost:8000/hello
HTTP/1.1 404 Not Found
Content-Length: 52
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:45:30 GMT
{
"code": "PAGE_NOT_FOUND",
"message": "Page not found"
}
# パスワード誤り
$ http --json POST localhost:8000/api/login email=test@gmail.com password=DUMMY
HTTP/1.1 401 Unauthorized
Content-Length: 55
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:50:51 GMT
Www-Authenticate: JWT realm=test zone
{
"code": 401,
"message": "incorrect Username or Password"
}
正常ケースを試してみます。
- ログインし、トークンを取得
- ログインユーザー情報の取得
- トークンのリフレッシュ
# ログイン
$ http --json POST localhost:8000/api/login email=test@gmail.com password=test
HTTP/1.1 200 OK
Content-Length: 232
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:51:11 GMT
{
"code": 200,
"expire": "2023-03-31T18:51:11+09:00",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODA3NzQ2NzEsImlkZW50aXR5IjoidGVzdEBnbWFpbC5jb20iLCJvcmlnX2lhdCI6MTY4MDE2OTg3MX0.Ggjzpb7LYMX1AUYWAN0SeWSXSg8PXqg10fa1tZ57sSc"
}
# ログインユーザー情報の取得
$ http --json GET localhost:8000/api/users/me --auth-type=bearer --auth="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODA3NzQ2NzEsImlkZW50aXR5IjoidGVzdEBnbWFpbC5jb20iLCJvcmlnX2lhdCI6MTY4MDE2OTg3MX0.Ggjzpb7LYMX1AUYWAN0SeWSXSg8PXqg10fa1tZ57sSc"
HTTP/1.1 200 OK
Content-Length: 27
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:51:40 GMT
{
"userID": "test@gmail.com"
}
# トークンリフレッシュ
$ http --json GET localhost:8000/api/refresh_token --auth-type=bearer --auth="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODA3NzQ2NzEsImlkZW50aXR5IjoidGVzdEBnbWFpbC5jb20iLCJvcmlnX2lhdCI6MTY4MDE2OTg3MX0.Ggjzpb7LYMX1AUYWAN0SeWSXSg8PXqg10fa1tZ57sSc"
HTTP/1.1 200 OK
Content-Length: 232
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Mar 2023 09:51:51 GMT
{
"code": 200,
"expire": "2023-03-31T18:51:51+09:00",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODA3NzQ3MTEsImlkZW50aXR5IjoidGVzdEBnbWFpbC5jb20iLCJvcmlnX2lhdCI6MTY4MDE2OTkxMX0.hfXHxGoP4GRe5QL6JxfQ27RiKBFzY3wN163rVh9V3Kw"
}
正しく動作しています。
簡単に実装できて満足です。
ひとこと
Go 言語で、Gin フレームワーク上に JWT 認証の API を実装できました。
便利ですね。
ディスカッション
コメント一覧
まだ、コメントがありません