Skip to content
<

在 gin 框架中使用 JWT 认证

什么是 JWT?

JWT 全称 JSON Web Token 是一种跨域认证解决方案,属于一个开放的标准,它规定了一种 Token 实现方式,目前多用于前后端分离项目和 OAuth2.0业务场景下。

为什么需要 JWT?

在之前的一些 web 项目中,我们通常使用的是Cookie-Session模式实现用户认证。相关流程大致如下:

  1. 用户在浏览器端填写用户名和密码,并发送给服务端
  2. 服务端对用户名和密码校验通过后会生成一份保存当前用户相关信息的 session 数据和一个与之对应的标识(通常称为 session_id)
  3. 服务端返回响应时将上一步的 session_id 写入用户浏览器的 Cookie
  4. 后续用户来自该浏览器的每次请求都会自动携带包含 session_id 的 Cookie
  5. 服务端通过请求中的 session_id 就能找到之前保存的该用户的那份 session 数据,从而获取该用户的相关信息。

这种方案依赖于客户端(浏览器)保存 Cookie,并且需要在服务端存储用户的 session 数据。

在移动互联网时代,我们的用户可能使用浏览器也可能使用 APP 来访问我们的服务,我们的 web 应用可能是前后端分开部署在不同的端口,有时候我们还需要支持第三方登录,这下Cookie-Session的模式就有些力不从心了。

JWT 就是一种基于 Token 的轻量级认证模式,服务端认证通过后,会生成一个 JSON 对象,经过签名后得到一个 Token(令牌)再发回给用户,用户后续请求只需要带上这个 Token,服务端解密之后就能获取该用户的相关信息了。

想要了解 JWT 的原理,推荐大家阅读:阮一峰的 JWT 入门教程

安装

我们使用 Go 语言社区中的 jwt 相关库来构建我们的应用,例如:https://github.com/golang-jwt/jwt

sh
go get github.com/golang-jwt/jwt/v4

本文将使用这个库来实现我们生成 JWT 和解析 JWT 的功能。

使用

默认 Claim

如果我们直接使用 JWT 中的默认的字段,没有其他定制化的需求则可以直接使用这个包中的方法快速生成和解析 token。

go
// 用户签名的字符串
var mySisningKey = []byte("mySigningKey")

// GenRegisteredClaims 使用默认生命的创建 jwt
func GenRegisteredClaims() (string, error) {
	// 创建 Claims
	claims := jwt.RegisteredClaims{
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
		Issuer:    "test",
	}
	// 生成 token 对象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 生成签名字符串
	return token.SignedString(mySisningKey)
}

// ParseRegisteredClaims 解析 jwt
func ValidatedRegisteredClaims(tokenString string) bool {
	// 解析 token
	token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
		return mySisningKey, nil
	})
	if err != nil { // 解析 token 失败
		return false
	}
	return token.Valid
}

自定义 Claims

我们需要定制自己的需求来决定 JWT 中保存哪些数据,比如我们规定在 JWT 中要存储username信息,那么我们就定义一个MyClaims结构体如下:

go
// CustomClaims 自定义声明类型 并内嵌 jwt.RegisteredClaims
// jwt 包自带的 jwt.RegisteredClaims 只包含了官方字段
// 假设我们这里需要额外记录一个 username 字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type CustomClaims struct {
	// 可根据需要自行添加字段
	Username             string `json:"username"`
	jwt.RegisteredClaims        // 内嵌标准的声明
}

然后我们定义 JWT 的过期时间,这里以24小时为例:

go
const TokenExpireDuration = time.Hour *24

接下来还需要定义一个用于签名的字符串:

go
// CustomSecret 用于加盐的字符串
var CustomSecret = []byte("夏天夏天悄悄过去")

生成 JWT

我们可以根据自己的业务需要封装一个生成 token 的函数。

go
// GenToken 生成 JWT
func GenToken(username string) (string, error) {
	// 创建一个我们自己的声明
	claims := &CustomClaims{
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpireDuration)),
			Issuer:    "my-project", // 签发人
		},
	}
	// 使用指定的签名方法创建签名对象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 使用指定的 secret 签名并获得完整的编码后的字符串 token
	return token.SignedString(CustomSecret)
}

解析 JWT

根据给定的 JWT 字符串,解析出数据。

go
// ParseToken 解析 JWT
func ParseToken(tokenString string) (*CustomClaims, error) {
	// 解析 token
	// 如果是自定义 Claim 结构体则需要使用 ParseWithClaims 方法
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, err error) {
		// 直接使用标准的 Claim 则可以直接使用 Parse 方法
		//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
		return CustomSecret, nil
	})
	if err != nil {
		return nil, err
	}
	// 对 token 对象中的 Claim 进行类型断言
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { // 校验 token
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

在 gin 框架中使用 JWT

首先我们注册一条路由/auth,对外提供获取 Token 的渠道:

go
r.POST("/auth", authHandler)

我们的authHandler定义如下:

go
func authHandler(c *gin.Context) {
	// 用户发送用户名和密码过来
	var user UserInfo
	err := c.ShouldBind(&user)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 2001,
			"msg": "无效的参数",
		})
	}
	// 校验用户名和密码是否正确
	if user.Username == "q1mi" && user.Password == "q1mi123" {
		// 生成 Token
		tokenString, _ := GenToken(user.Username)
		c.JSON(http.StatusOK, gin.H{
			"code": 200,
			"msg": "success",
			"data": gin.H{
				"token": tokenString,
			},
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"code": 2002,
		"msg": "鉴权失败",
	})
}

用户通过上面的接口获取 Token 之后,后续就会携带着 Token 再来请求我们的其他接口,这个时候就需要对这些请求的 Token 进行校验操作了,很显然我们应该实现一个检验 Token 的中间件,具体实现如下:

go
// JWTAuthMiddleware 基于 JWT 的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {
		// 客户端携带 Token 有三种方式 1.放在请求头 2.放在请求体 3.放在 URI
		// 这里假设 Token 放在 Header 的 Authorization 中,并使用 Bearer 开头
		// 这里的具体实现方式要依据你的实际业务情况决定
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusOK, gin.H{
				"code": 2003,
				"msg": "请求头中 auth 为空",
			})
			c.Abort()
			return
		}
		// 按空格分割
		parts := strings.SplitN(authHeader, " ", 2)
		if !(len(parts) == 2 && parts[0] == "Bearer") {
			c.JSON(http.StatusOK, gin.H{
				"code": 2004,
				"msg": "请求头中 auth 格式有误",
			})
			c.Abort()
			return
		}
	}
}

注册一个/home路由,发个请求验证一下吧。

go
r.GET("/home", JWTAuthMiddleware(), homeHandler)

func homeHandler(c *gin.Context) {
	username := c.MustGet("username").(string)
	c.JSON(http.StatusOK, gin.H{
		"code": 2000,
		"msg": "success",
		"data": gin.H{"username": username},
	})
}

如果不想自己实现上述功能,你也可以使用 Github 上别人封装好的包,比如https://github.com/appleboy/gin-jwt

refresh token

在某些业务场景下,我们可能还需要使用 refresh token。

这里可以参考RFC 6749 OAuth2.0中关于 refresh token 的介绍