Gin-Jwt简单使用

安装

使用 Go Modules(推荐)

1
2
export GO111MODULE=on
go get github.com/appleboy/gin-jwt/v2
1
import "github.com/appleboy/gin-jwt/v2"

快速开始示例

请参考官方 _example/basic/server.go 示例文件,并可使用 ExtractClaims 获取 JWT 内的用户数据

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package main

import (
	"log"
	"net/http"
	"os"
	"time"

	jwt "github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
)

type login struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

var (
	identityKey = "id"
	port        string
)

// User demo
type User struct {
	UserName  string
	FirstName string
	LastName  string
}

func init() {
	port = os.Getenv("PORT")
	if port == "" {
		port = "8000"
	}
}

func main() {
	engine := gin.Default()
	// the jwt middleware
	authMiddleware, err := jwt.New(initParams())
	if err != nil {
		log.Fatal("JWT Error:" + err.Error())
	}

	// register middleware
	engine.Use(handlerMiddleWare(authMiddleware))

	// register route
	registerRoute(engine, authMiddleware)

	// start http server
	if err = http.ListenAndServe(":"+port, engine); err != nil {
		log.Fatal(err)
	}
}

func registerRoute(r *gin.Engine, handle *jwt.GinJWTMiddleware) {
	r.POST("/login", handle.LoginHandler)
	r.NoRoute(handle.MiddlewareFunc(), handleNoRoute())

	auth := r.Group("/auth", handle.MiddlewareFunc())
	auth.GET("/refresh_token", handle.RefreshHandler)
	auth.GET("/hello", helloHandler)
}

func handlerMiddleWare(authMiddleware *jwt.GinJWTMiddleware) gin.HandlerFunc {
	return func(context *gin.Context) {
		errInit := authMiddleware.MiddlewareInit()
		if errInit != nil {
			log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
		}
	}
}

func initParams() *jwt.GinJWTMiddleware {
	return &jwt.GinJWTMiddleware{
		//含义: 认证领域的名称。它会出现在 WWW-Authenticate 响应头中,主要用于浏览器弹出的认证窗口(虽然在 API 认证中很少直接用到),可以看作是你这个认证系统的一个标识名称。
		Realm:       "test zone",
		//含义: JWT 密钥 (Secret Key)。这是整个认证系统的核心机密,用于对 JWT 进行签名和验证。所有生成的 Token 都会用这个密钥签名,所有接收到的 Token 也必须用同一个密钥来验证签名,以确保 Token 没有被篡改。这个密钥绝对不能泄露。
		Key:         []byte("secret key"),
		//含义: 指定用来获取当前时间的函数。默认就是 time.Now,一般不需要修改。但在测试时,你可以把它换成一个固定的时间函数,方便测试 Token 的过期逻辑。
		TimeFunc:      time.Now,
		//含义: Token 的有效期。从签发那一刻开始,一个 Token 能有效使用多长时间。这里是从配置文件读取一个小时数为单位的超时时间。
		Timeout:     time.Hour,
		//含义: 最大刷新时间。即使 Token 已经过期,只要没超过这个 MaxRefresh 的时间点,用户就仍然可以通过旧 Token 来刷新获取一个新 Token,而不需要重新登录。这是一种“续期”机制。如果超过了 MaxRefresh,就必须重新输入用户名密码登录。
		MaxRefresh:  time.Hour,
		//含义: 一个键 (key),用于在 Gin 的 context 中存储用户信息。当用户认证成功后,IdentityHandler 返回的用户信息会以这个键为名,存储在 c.Keys 中。后续的业务处理器可以通过 c.Get("your_identity_key") 来获取当前登录的用户信息。
		IdentityKey: identityKey,
		//含义: 生成 Token 负载 (Payload) 的函数。当用户登录成功时,这个函数会被调用。它的作用是决定哪些用户信息需要被存放到 Token 里。通常我们会把用户 ID、用户名等信息放进去。
		PayloadFunc: payloadFunc(),
		//含义: 解析 Token 负载的函数。当一个请求携带 Token 访问受保护的接口时,中间件会先验证 Token 的合法性,然后调用这个函数。它的作用是从已经解析的 Token 负载中提取出用户的身份信息,并返回。这个返回值会被存入 Gin 的 context 中,供后续使用。
		IdentityHandler: identityHandler(),
		//含义: 认证器 (Authenticator)。这是处理登录请求的函数。它接收登录时提交的数据(比如用户名和密码),然后去数据库里校验。如果校验成功,它需要返回用户信息;如果失败,则返回错误。gin-jwt 会接着调用 PayloadFunc 来为这个用户生成 Token。
		Authenticator:   authenticator(),
		//含义: 授权器 (Authorizator)。当用户访问一个受保护的接口时,在 IdentityHandler 执行之后,这个函数会被调用。它的作用是进行权限判断。你可以根据从 IdentityHandler 获取到的用户信息,判断这个用户是否有权限访问当前请求的资源。如果返回 true,则放行;如果返回 false,则拒绝访问。
		Authorizator:    authorizator(),
		//含义: 自定义认证失败的响应。当认证过程(包括 Token 缺失、格式错误、过期、验证失败、授权失败等)出现任何问题时,这个函数会被调用来生成返回给客户端的错误信息。
		Unauthorized:    unauthorized(),
		//含义: 定义在哪里寻找 Token。这是一个非常灵活的配置,告诉中间件按顺序从三个地方查找 Token:
		TokenLookup:     "header: Authorization, query: token, cookie: jwt",
		// TokenLookup: "query:token",
		// TokenLookup: "cookie:token",
		TokenHeadName: "Bearer",
	}
}

func payloadFunc() func(data interface{}) jwt.MapClaims {
	return func(data interface{}) jwt.MapClaims {
		if v, ok := data.(*User); ok {
			return jwt.MapClaims{
				identityKey: v.UserName,
			}
		}
		return jwt.MapClaims{}
	}
}

func identityHandler() func(c *gin.Context) interface{} {
	return func(c *gin.Context) interface{} {
		claims := jwt.ExtractClaims(c)
		return &User{
			UserName: claims[identityKey].(string),
		}
	}
}

func authenticator() func(c *gin.Context) (interface{}, error) {
	return func(c *gin.Context) (interface{}, error) {
		var loginVals login
		if err := c.ShouldBind(&loginVals); err != nil {
			return "", jwt.ErrMissingLoginValues
		}
		userID := loginVals.Username
		password := loginVals.Password

		if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
			return &User{
				UserName:  userID,
				LastName:  "Bo-Yi",
				FirstName: "Wu",
			}, nil
		}
		return nil, jwt.ErrFailedAuthentication
	}
}

func authorizator() func(data interface{}, c *gin.Context) bool {
	return func(data interface{}, c *gin.Context) bool {
		if v, ok := data.(*User); ok && v.UserName == "admin" {
			return true
		}
		return false
	}
}

func unauthorized() func(c *gin.Context, code int, message string) {
	return func(c *gin.Context, code int, message string) {
		c.JSON(code, gin.H{
			"code":    code,
			"message": message,
		})
	}
}

func handleNoRoute() func(c *gin.Context) {
	return func(c *gin.Context) {
		claims := jwt.ExtractClaims(c)
		log.Printf("NoRoute claims: %#v\n", claims)
		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
	}
}

func helloHandler(c *gin.Context) {
	claims := jwt.ExtractClaims(c)
	user, _ := c.Get(identityKey)
	c.JSON(200, gin.H{
		"userID":   claims[identityKey],
		"userName": user.(*User).UserName,
		"text":     "Hello World.",
	})
}

如需将 JWT 设置于 Cookie,请使用以下选项(参考 MDN 文档):

1
2
3
4
5
6
7
SendCookie:       true,
SecureCookie:     false, // 非 HTTPS 开发环境
CookieHTTPOnly:   true,  // JS 无法修改
CookieDomain:     "localhost:8080",
CookieName:       "token", // 默认 jwt
TokenLookup:      "cookie:token",
CookieSameSite:   http.SameSiteDefaultMode, // SameSiteDefaultMode, SameSiteLaxMode, SameSiteStrictMode, SameSiteNoneMode

登录流程(LoginHandler)

  • 内置: LoginHandler 在登录端点调用此函数以触发登录流程。

  • 必须: Authenticator 验证 Gin context 内的用户凭证。验证成功后返回要嵌入 JWT Token 的用户数据(如账号、角色等)。失败则调用 Unauthorized。

  • 可选: PayloadFunc 将认证通过的用户数据转为 MapClaims(map[string]interface{}),必须包含 IdentityKey(默认 “identity”)。

  • 可选: LoginResponse 处理登录后逻辑,例如返回 Token JSON。

需要 JWT Token 的端点(MiddlewareFunc)

  • 内置: MiddlewareFunc 用于需要 JWT 认证的端点。会:

    • 从 header/cookie/query 解析 Token
    • 验证 Token
    • 调用 IdentityHandler 与 Authorizator
    • 验证失败则调用 Unauthorized
  • 可选: IdentityHandler 从 JWT Claims 获取用户身份。

  • 可选: Authorizator 检查用户是否有权限访问该端点。

登出流程(LogoutHandler)

  • 内置: LogoutHandler 用于登出端点。会清除 Cookie(若 SendCookie 设置为 true)并调用 LogoutResponse。

  • 可选: LogoutResponse 返回登出结果的 HTTP 状态码。

刷新流程(RefreshHandler)

  • 内置: RefreshHandler 用于刷新 Token 端点。若 Token 在 MaxRefreshTime 内,会发新 Token 并调用 RefreshResponse。

  • 可选: RefreshResponse 返回新 Token 的 JSON。

登录失败、Token 错误或权限不足

  • 可选: Unauthorized 处理登录、授权或 Token 错误时的响应。返回 HTTP 错误码与消息的 JSON。

来源官方文档

0%