在前后端分离的项目中,越来越多的项目采用 JWT 代替传统的 cookie ,这里我们来使用 JWT 结合 Gin 来作为一个登录授权和权限校验。

🔑什么是 JWT

JWT 的全称叫做 JSON WEB TOKEN,在目前前后端系统中使用较多。

JWT 构成

JWT 是由三段构成的。分别是 HEADER,PAYLOAD,VERIFY SIGNATURE,它们生成的信息通过 . 分割。

header 是由 一个 typalg 组成,typ 会指明为 JWT,而 alg 是所使用的加密算法。

{
  "alg": "HS256",
  "typ": "JWT"
}

PAYLOAD

payload 是 JWT 的载体,也就是我们要承载的信息。这段信息是我们可以自定义的,可以定义我们要存放什么信息,那些字段。该部分信息不宜过多,它会影响 JWT 生成的大小,还有就是请勿将敏感数据存入该部分,该端数据前端是可以解析获取 token 内信息的。

官方给了七个默认字段,我们可以不全部使用,也可以加入我们需要的字段。

名称 含义
Audience 表示JWT的受众
ExpiresAt 失效时间
Id 签发编号
IssuedAt 签发时间
Issuer 签发人
NotBefore 生效时间
Subject 主题

VERIFY SIGNATURE

这也是 JWT 的最后一段,该部分是由算法计算完成的。

对刚刚的 header 进行 base64Url 编码,对 payload 进行 base64Url 编码,两端完成编码后通过 . 进行连接起来。

base64UrlEncode(header).base64UrlEncode(payload)

完成上述步骤后,就要通过我们 header 里指定的加密算法对上部分进行加密,同时我们还要插入我们的一个密钥,来确保我的 JWT 签发是安全的。

这便是我们的第三部分。

当三部分都完成后,通过使用 . 将三部分分割,生成了上图所示的 JWT 。

JWT 登录原理

简单的说就是当用户登录的时候,服务器校验登录名称和密码是否正确,正确的话,会生成 JWT 返回给客户端。客户端获取到 JWT 后要进行保存,之后的每次请求都会讲 JWT 携带在头部,每次服务器都会获取头部的 JWT 是否正确,如果正确则正确执行该请求,否者验证失败,重新登录。

🔒Gin 生成 JWT

go 语言的 JWT 库有很多。jwt.io 上也给出了很多 。这里使用 jwt-go

"github.com/dgrijalva/jwt-go"

我们对登录方法进行改造。

// 省略代码
expiresTime := time.Now().Unix() + int64(config.OneDayOfHours)
claims := jwt.StandardClaims{
    Audience:  user.Username,     // 受众
    ExpiresAt: expiresTime,       // 失效时间
    Id:        string(user.ID),   // 编号
    IssuedAt:  time.Now().Unix(), // 签发时间
    Issuer:    "gin hello",       // 签发人
    NotBefore: time.Now().Unix(), // 生效时间
    Subject:   "login",           // 主题
}
var jwtSecret = []byte(config.Secret)
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 省略代码

这里的 config.OneDayOfHours 设定了过期时间,这里设定了一天。通过 StandardClaims 生成标准的载体,也就是上文提到的七个字段,其中 编号设定为 用户 id。其中的 jwtSecret 是我们设定的密钥,

我们这里通过 HS256 算法生成 tokenClaims ,这就是我们的 HEADER 部分和 PAYLOAD。

token, err := tokenClaims.SignedString(jwtSecret)

这样便生成了我们的 token 。我们要将我们的 token 和 Bearer 拼接在一起,同时中间用空格隔开。

token =  "Bearer "+ token

生成 Bearer Token 。

当我们用户进行登录的时候,就可以通过该片段生成 JWT。

下面是完整代码:

func CreateJwt(ctx *gin.Context) {
	// 获取用户
	user := &model.User{}
	result := &model.Result{
		Code:    200,
		Message: "登录成功",
		Data:    nil,
	}
	if e := ctx.BindJSON(&user); e != nil {
		result.Message = "数据绑定失败"
		result.Code = http.StatusUnauthorized
		ctx.JSON(http.StatusUnauthorized, gin.H{
			"result": result,
		})
	}
	u := user.QueryByUsername()
	if u.Password == user.Password {
		expiresTime := time.Now().Unix() + int64(config.OneDayOfHours)
		claims := jwt.StandardClaims{
			Audience:  user.Username,     // 受众
			ExpiresAt: expiresTime,       // 失效时间
			Id:        string(user.ID),   // 编号
			IssuedAt:  time.Now().Unix(), // 签发时间
			Issuer:    "gin hello",       // 签发人
			NotBefore: time.Now().Unix(), // 生效时间
			Subject:   "login",           // 主题
		}
		var jwtSecret = []byte(config.Secret)
		tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
		if token, err := tokenClaims.SignedString(jwtSecret); err == nil {
			result.Message = "登录成功"
			result.Data = "Bearer " + token
			result.Code = http.StatusOK
			ctx.JSON(result.Code, gin.H{
				"result": result,
			})
		} else {
			result.Message = "登录失败"
			result.Code = http.StatusOK
			ctx.JSON(result.Code, gin.H{
				"result": result,
			})
		}
	} else {
		result.Message = "登录失败"
		result.Code = http.StatusOK
		ctx.JSON(result.Code, gin.H{
			"result": result,
		})
	}
}

通过 .http 请求测试,结果如下

{
  "result": {
    "code": 200,
    "message": "登录成功",
    "data": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjMiLCJleHAiOjE1NjQ3OTY0MzksImp0aSI6Ilx1MDAwMCIsImlhdCI6MTU2NDc5NjQxOSwiaXNzIjoiZ2luIGhlbGxvIiwibmJmIjoxNTY0Nzk2NDE5LCJzdWIiOiJsb2dpbiJ9.CpacmfBSMgmK2TgrT-KwNB60bsvwgyryGQ0pWZr8laU"
  }
}

这个便完成了token的生成。

🔐Gin 校验 Token

那么,接下来就需要完成 token 的验证。

还记得之前我们验证用户是否授权采用的办法吗?是的,在中间件里查看用户 cookie。同样的方法,我们这里校验用户 JWT 是否有效。

编写我们的中间件。

新建立 middleware/Auth.go

首先先编写我们的解析 token 方法,parseToken()

func parseToken(token string) (*jwt.StandardClaims, error) {
	jwtToken, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return []byte(config.Secret), nil
	})
	if err == nil && jwtToken != nil {
		if claim, ok := jwtToken.Claims.(*jwt.StandardClaims); ok && jwtToken.Valid {
			return claim, nil
		}
	}
	return nil, err
}

通过传入我们的 token , 来对 token 进行解析。

完整的中间件代码

func Auth() gin.HandlerFunc {
	return func(context *gin.Context) {
		result := model.Result{
			Code:    http.StatusUnauthorized,
			Message: "无法认证,重新登录",
			Data:    nil,
		}
		auth := context.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			context.Abort()
			context.JSON(http.StatusUnauthorized, gin.H{
				"result": result,
			})
		}
		auth = strings.Fields(auth)[1]
		// 校验token
		_, err := parseToken(auth)
		if err != nil {
			context.Abort()
			result.Message = "token 过期" + err.Error()
			context.JSON(http.StatusUnauthorized, gin.H{
				"result": result,
			})
		} else {
			println("token 正确")
		}
		context.Next()
	}
}

首先在请求头获取 token ,然后对先把 token 进行解析,将 Bearer 和 JWT 拆分出来,将 JWT 进行校验。

我们只需要对我们需要校验的路由进行添加中间件校验即可。

	router.GET("/", middleware.Auth(), func(context *gin.Context) {
		context.JSON(http.StatusOK, time.Now().Unix())
	})

当我们访问 / 的时候就需要携带 token 了

GET http://localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjMiLCJleHAiOjE1NjQ3OTQzNjIsImp0aSI6Ilx1MDAwMCIsImlhdCI6MTU2NDc5NDM0MiwiaXNzIjoiZ2luIGhlbGxvIiwibmJmIjoxNTY0Nzk0MzQyLCJzdWIiOiJsb2dpbiJ9.uQxGMsftyVFtYIGwQVm1QB2djw-uMfDbw81E5LMjliU

✍总结

本章节对什么是 JWT,Gin 中如何使用 JWT 做了介绍。但是不要过于迷信 JWT,JWT 还有很多问题,比如说 JWT 失效只能是时间过期,如果修改密码或者账户注销等操作需要我们另外添加逻辑判断。适合的地方选用适合的技术才能发挥最大的优势。

👨‍💻本章节代码

Github