1. 介绍

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

jwt-go 是使用Go语言实现的Json web token (JWT),目前GitHub Start 9.8k,源码地址: https://github.com/dgrijalva/jwt-go,从版本3.2.1开始,源码地址变更为: https://github.com/golang-jwt/jwt,需要下载最新版本时,可以使用这个地址。

1.2 集成示意图

image-20210723174102427

2. 配置

2.1 编辑主配置

文件位置:./config.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app:
  ...
log:
  ...
mysql:
  ...
jwt:
  secret: shershon # jwt生成密钥
  issuer: 唐小山 # 签发人
  expire: 3h # 有效时间,值如: 30s|10min|1h

2.2 新增结构体

文件位置: ./config/jwt.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * @Description JWT配置
 **/
package config

import "time"
// JSON WEB TOKEN 配置
type jwt struct {
    Secret string        `yaml:"secret"`
    Issuer string        `yaml:"issuer"`
    Expire time.Duration `yaml:"expire"`
}

3. 编辑中间件

文件位置:./middleware/jwt.go,功能包括有中间件函数/创建Token/解析Token

3.1 中间件函数

 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
package middleware

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "shershon1991/gin-api-template/global"
    "shershon1991/gin-api-template/internal"
    "shershon1991/gin-api-template/model/dao"
    "shershon1991/gin-api-template/model/request"
    "shershon1991/gin-api-template/model/response"
)

/**
 * @description: JWT中间件
 * @return func(ctx *gin.Context)
 */
func JWTAuthMiddleware() func(ctx *gin.Context) {
    return func(ctx *gin.Context) {
        // 获取参数中的token
        token := getToken(ctx)
        global.GvaLogger.Sugar().Infof("token: %s", token)
        if token == "" {
            response.Error(ctx, "Token不能为空!")
            // 中断请求
            ctx.Abort()
            return
        }
        // 验证Token
        userClaim, err := internal.ParseToken(token)
        if err != nil {
            response.ErrorWithToken(ctx, "Token error :"+err.Error())
            // 中断请求
            ctx.Abort()
            return
        }
        // 设置到上下文中
        setContextData(ctx, userClaim, token)
        // 继续请求后续流程
        ctx.Next()
    }
}

// 设置数据到上下文
func setContextData(ctx *gin.Context, userClaim *request.UserClaims, token string) {
    userDao := &dao.UserDao{
        Uid: userClaim.Uid,
    }
    user, err := userDao.FindUser()
    if err != nil {
        response.Error(ctx, "用户不存在!")
        // 中断请求
        ctx.Abort()
        return
    }
    user.Token = token
    ctx.Set("userClaim", userClaim)
    ctx.Set("user", user)
}

// 从请求中获取Token
func getToken(ctx *gin.Context) string {
    var token string
    // 从header中获取
    token = ctx.Request.Header.Get("TOKEN")
    if token != "" {
        return token
    }
    // 获取当前请求方法
    if ctx.Request.Method == http.MethodGet {
        // 从Get请求中获取Token
        token, ok := ctx.GetQuery("token")
        if ok {
            return token
        }
    }
    // 从POST中和获取
    if ctx.Request.Method == http.MethodPost {
        // 从Get请求中获取Token
        postParam := make(map[string]interface{})
        _ = ctx.ShouldBindJSON(&postParam)
        token, ok := postParam["token"]
        if ok {
            return token.(string)
        }
    }
    return ""
}

3.2 创建Token

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 创建Jwt
func CreateToken(uid uint) (string, error) {
    newWithClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, &request.UserClaims{
        StandardClaims: &jwt.StandardClaims{
            ExpiresAt: time.Now().Add(global.GvaConfig.Jwt.Expire).Unix(), // 有效期
            Issuer:    global.GvaConfig.Jwt.Issuer,                  // 签发人
            IssuedAt:  time.Now().Unix(),                            // 签发时间
        },
        Uid: uid,
    })
    return newWithClaims.SignedString([]byte(global.GvaConfig.Jwt.Secret))
}

3.3 解析Token

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 验证JWT
func ParseToken(tokenString string) (*request.UserClaims, error) {
    var err error
    var token *jwt.Token
    token, err = jwt.ParseWithClaims(tokenString, &request.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(global.GvaConfig.Jwt.Secret), nil
    })
    if err != nil {
        global.GvaLogger.Error("解析JWT失败", zap.String("error", err.Error()))
        return nil, err
    }
    // 断言
    userClaims, ok := token.Claims.(*request.UserClaims)
    // 验证
    if !ok || !token.Valid {
        return nil, errors.New("JWT验证失败")
    }
    return userClaims, nil
}

4. 注册路由

注册路由流程

4.1 不需要登录路由

1. 注册路由

文件位置:router/user_router.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * @description: 用户相关的路由
 * @param engine
 */
func InitUserRouter(engine *gin.Engine) {
    // 不需要登录的路由
    noLoginGroup := engine.Group("v1/user")
    {
        // 登录
        noLoginGroup.POST("login", v1.Login)
    }
}

2. 路由绑定函数

文件位置:./api/v1/user_api.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * @description: TODO 用户账号密码登录
 * @param ctx
 */
func Login(ctx *gin.Context) {
    // 绑定参数
    var loginParam request.LoginParam
    _ = ctx.ShouldBindJSON(&loginParam)
    //...(省略)
    // 生成token
    token, err := middleware.CreateToken(userRecord.ID)
    if err != nil {
        global.GvaLogger.Sugar().Errorf("登录失败,Token生成异常:%s", err)
        response.Error(ctx, "登录失败,账号或者密码错误!")
        return
    }
    userRecord.Token = token
    response.OkWithData(ctx, userRecord)
}

3. 请求返回

4.2 需要登录路由

1. 注册路由

文件位置:router/user_router.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * @description: 用户相关的路由
 * @param engine
 */
func InitUserRouter(engine *gin.Engine) {
    // 不需要登录的路由
    ...
    // 需要登录
    tokenGroup := engine.Group("v1/user").Use(middleware.JWTAuthMiddleware())
    {
        tokenGroup.POST("/detail", v1.GetUser)
    }
}

2. 路由绑定函数

文件位置:./api/v1/user_api.go

1
2
3
4
5
6
7
// 查询用户信息
func GetUser(ctx *gin.Context) {
    // 从上下文中获取用户信息,(经过中间件逻辑时,已经设置到上下文)
    user, _ := ctx.Get("user")
    response.OkWithData(ctx, user)
    return
}

3. 请求返回