Gin框架中JWT的使用初探

2019-08-06

0. 前言

最近学习Golang和Gin框架,初步了解了一下JWT作身份校验,这里做个总结。

1. 认识JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

header 用于描述元信息,例如产生signature的算法:

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

其中 alg 指定了使用哪种哈希算法来创建signature

payload 用于携带你希望向服务端传递的信息。你既可以往里添加官方字段(这里的“字段” (field) 也可以被称作“声明” claims),例如iss(Issuer), sub(Subject), exp(Expiration time),也可以塞入自定义的字段,比如 email:

{
    "email": "who@mail.com"
}

signature 的创建分为:

  1. 拿到密钥 secret
  2. header 进行 base64 编码,结果为 headerStr
  3. payload 进行 base64 编码,结果为 payloadStr
  4. headerStrpayloadStr. 拼接为字符串 data
  5. datasecret 作为参数,用哈希算法计算出签名

2. Token 的生成和解析

2.1 生成 token

首先引入 jwt-go 依赖

import "github.com/dgrijalva/jwt-go"

自定义 payload 声明

type Claims struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    jwt.StandardClaims
}

自定义 secret 密钥

var jwtSecret = []byte("mySecret")

生成 token

func GenerateToken(email, password string) (string, error) {
    nowTime := time.Now()
    expireTime := nowTime.Add(2 * time.Hour) // 失效时间
    claims := Claims{
        email,
        password,
        jwt.StandardClaims{
            ExpiresAt: expireTime.Unix(),
            Issuer:    "custom-issuer",
        },
    }
    tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    token, err := tokenClaims.SignedString(jwtSecret)
    return token, err
}

2.2 解析 token

func ParseToken(token string) (*Claims, error) {
    tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })
    if tokenClaims != nil {
        // tokenClaims.Valid 判断expireTime
        if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
            return claims, nil
        }
    }
    return nil, err
}

3. JWT 中间件的使用

3.1 编写中间件函数

func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.Request.Header.Get("token")
        if token == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "code": 40001,
                "msg":  "请求未携带token, 无权限访问",
            })
            c.Abort()
            return
        }

        claims, err := ParseToken(token)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "code": 40001,
                "msg":  err.Error(),
            })
            c.Abort()
            return
        }

        // check if token is valid
        if claims == nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "code": 40001,
                "msg":  "token error",
            })
            c.Abort()
            return
        }

        // 查询数据库,判断token携带的用户信息是否有效
        if authErr := repository.Auth(claims.Email, claims.Password); authErr != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "code": 40001,
                "msg":  "token error",
            })
            c.Abort()
            return
        }
        c.Set("claims", claims)
    }
}

repository.Auth() 定义为

func Auth(email, password string) error {
    var user model.User
    var err error
    if err = DB.Where("email = ?", email).First(&user).Error; err != nil {
        return err
    }
    if user.Password != password {
        return errors.New("incorrect email or password")
    }
    return nil
}

3.2 JWT 的使用

func main() {
    r := gin.Default()
    r.Use(JWTAuth())
}

这样,所有路由都需经过JWT验证,也可以对某一 routerGroup 的接口作身份验证

Gin

Jeff Liu

Express.js 中 cookie 和 session 的使用

Mac下安装Docker及MySQL