Gin中使用jwt-go实现JWT鉴权登陆

发布时间 2023-12-23 23:32:43作者: 李若盛开

在Go语言中,JWT(JSON Web Token)鉴权可以使用第三方库来实现,比如jwt-go。

库的介绍和使用可见文档:jwt package - github.com/golang-jwt/jwt/v5 - Go Packages

创建JWT令牌

在服务器中,可以使用以下代码创建JWT令牌

package middleware
 
import (
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
    "mybili/serializer"
    "mybili/utils"
    "net/http"
    "os"
    "time"
)
 
type MyClaims struct {
    Username string `json:"user_name"`
    //Password string `json:"password"`
    jwt.RegisteredClaims
}
 
// 生成token
func SetToken(username string) (string, int) {
    SetClaims := MyClaims{
        Username: username,
        //Password: password,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), //有效时间
            IssuedAt:  jwt.NewNumericDate(time.Now()),                     //签发时间
            NotBefore: jwt.NewNumericDate(time.Now()),                     //生效时间
            Issuer:    os.Getenv("JWT_ISSUER"),                            //签发人
            Subject:   "somebody",                                         //主题
            ID:        "1",                                                //JWT ID用于标识该JWT
            Audience:  []string{"somebody_else"},                          //用户
        },
    }
 
    //使用指定的加密方式和声明类型创建新令牌
    tokenStruct := jwt.NewWithClaims(jwt.SigningMethodHS256, SetClaims)
    //获得完整的、签名的令牌
    token, err := tokenStruct.SignedString([]byte(os.Getenv("JWT_KEY")))
    if err != nil {
        utils.Logger.Errorf("err:%v", err.Error())
        return "", utils.TOKEN_CREATE_FAILED
    }
    return token, utils.SUCCESS
}

jwt.RegisteredClaims是对标准注册声明的封装,您可以单独使用它,也可以把它嵌入到自定义类型中,以提供标准验证功能。

jwt.NewWithClaims使用指定的签名方法和声明创建一个新令牌。

创建了一个名为MyClaims 的结构体,用于定义JWT负载。除了标准验证选项外,我们也可以自定义Payload有效载荷字段,例如将用户名Username放到payload中。

JWT验证

验证JWT可以使用如下代码实现:

// 验证token
func CheckToken(token string) (*MyClaims, int) {
    //解析、验证并返回token。
    tokenObj, err := jwt.ParseWithClaims(token, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(os.Getenv("JWT_KEY")), nil
    })
 
    if err != nil {
        utils.Logger.Errorf("err:%v", err.Error())
        return nil, utils.ERROR
    }
 
    if claims, ok := tokenObj.Claims.(*MyClaims); ok && tokenObj.Valid {
        fmt.Printf("%v %v\n", claims.Username, claims.RegisteredClaims)
        return claims, utils.SUCCESS
    } else {
        return nil, utils.ERROR
    }
}

jwt.ParseWithClaims是NewParser().ParseWithClaims()的快捷方式,第一个参数是token的string值,第二个参数是我们之后需要把解析的数据放入的地方,第三个参数将被Parse方法用作回调函数,以提供用于验证的键。函数接收已解析但未验证的令牌。

解析结果输出结果如下:

Username:mjiarong
RegisteredClaims:{mybili somebody [somebody_else] 2023-10-02 20:25:30 +0800 CST 2023-10-01 20:25:30 +0800 CST 2023-10-01 20:25:30 +0800 CST 1}

JWT中间件

在中间件中服务端会获取用户token,基于jwt校验是否合法,合法放行,否则拒绝。

// jwt中间件
func JwtToken() gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenHeader := c.Request.Header.Get("Authorization")
        code := utils.SUCCESS
        if tokenHeader == "" {
            code = utils.TOKEN_NOT_EXIST
            c.JSON(http.StatusOK, serializer.CheckToken(
                code,
                utils.GetErrMsg(code)))
            c.Abort()
            return
        }
 
        key, tCode := CheckToken(tokenHeader)
        if tCode == utils.ERROR {
            code = utils.TOKEN_WRONG
            c.JSON(http.StatusOK, serializer.CheckToken(
                code,
                utils.GetErrMsg(code)))
            c.Abort()
            return
        }
 
        //判断token是否过期
        if time.Now().Unix() > key.ExpiresAt.Unix() {
            code = utils.TOKEN_RUNTIME
            c.JSON(http.StatusOK, serializer.CheckToken(
                code,
                utils.GetErrMsg(code)))
            c.Abort()
            return
        }
 
        c.Set("username", key.Username)
        c.Next()
    }
}

前端JWT登录鉴权的实现 

一般情况下,客户端在登录后得到了一个JWT方式的token。那这个token放哪里呢?最好把JWT放在HTTP请求的Header Authorization,格式是Authorization: Bearer jwtStr。那是否意味着前端的每个请求都附带后端返回的token吗?显然是否定的,我们只需要把需要鉴权登陆的请求头中带上token就行了。对此我们可以使用axios的请求拦截器来对请求进行过滤,以下是作者自己项目中的一个请求拦截器的实现:

import axios from "axios";
 
let http = axios.create({
    baseURL: process.env.VUE_APP_BASE_API, //配置默认的地址
    withCredentials: true, //将会默认携带认证给后端
    timeout: 1000 * 10, //请求超时设置,如果超过了10秒,那么就会进入reject
});
 
http.interceptors.request.use(
    //axios的请求拦截器,它可以拦截所有的请求,为所有的请求添加逻辑
    //拦截了请求后,如果不放行,那么所有的请求会一直被拦截,因此需要return不需要拦截的请求。
    function(config) {
        let postWhiteList = [
            "/user/login",
            "/user/register",
        ]; //将不需要拦截的请求拿出来
        let getWhiteList = [
            "/rank/daily",
            "/comment",
            "/upload/token",
            "/upload/credentials",
            "/comment",
        ];
 
        if (config.method === 'post' && postWhiteList.includes(config.url)) {
            //如果当前的请求地址中,包含在不需要拦截请求地址中,那么就放行
            return config;
        } else if (config.method === 'get' && (getWhiteList.includes(config.url) || config.url.includes("/video"))) {
            return config;
        } else {
            //如果是需要被拦截的请求
            let token = window.sessionStorage.getItem("token") || ""; //将登录成功后,在后端中返回来的token从本地存储取出来
            config.headers["Authorization"] = token; //给需要拦截的请求中请求头添加token。config.headers["authorization"]是一个固定的写法
            return config; //添加后,放行
        }
    }
);

对此,前后端中对JWT的实现就基本完成了。

总结 

JWT作为一种安全、简单的认证方式,被广泛应用于Web开发中。JWT不仅可以用于用户认证,还可以用于数据传输、API认证等场景。在实际应用中,应该注意JWT的有效期,以及签名密钥的安全保护。任何技术都不是完美的,JWT 也有很多缺陷,具体是选择 JWT 还是 Session 方案还是要看项目的具体需求。