34-jwt使用

33 阅读4分钟

安装jwt 包

  • "github.com/golang-jwt/jwt/v4"
  • 简单的 jwt 生成
package utils

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

// 用于签名的字符串
var mySigningKey = []byte("jiangxiaobai.com")

// 使用默认声明创建jwt
// 如果我们直接使用JWT中默认的字段,没有其他定制化的需求则可以直接使用这个包中的和方法快速生成和解析token
func GenRegisteredClaims() (string, error) {
	// 创建 Claims
	claims := &jwt.RegisteredClaims{
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 过期时间
		Issuer:    "qimi",                                             // 签发人
	}
	// 生成token对象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 生成签名字符串
	return token.SignedString(mySigningKey)
}

// 解析jwt
func ValidateRegisteredClaims(tokenString string) bool {
	// 解析token
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		return mySigningKey, nil
	})
	if err != nil { // 解析token失败
		return false
	}
	return token.Valid
}
  • 自定义生成jwt 主要是需要加一下自定一个字段如 username
package utils

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

// 自定义Claims
// 我们需要定制自己的需求来决定JWT中保存哪些数据,比如我们规定在JWT中要存储username信息,那么我们就定义一个MyClaims结构体如下:
// CustomClaims 自定义声明类型 并内嵌jwt.RegisteredClaims
// jwt包自带的jwt.RegisteredClaims只包含了官方字段
// 假设我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type CustomClaims struct {
	// 可根据需要自行添加字段
	Username             string `json:"username"`
	jwt.RegisteredClaims        // 内嵌标准的声明
}

const TokenExpireDuration = time.Hour * 24

// CustomSecret 用于加盐的字符串
var CustomSecret = []byte("爱你没差这一点时差")

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

// 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中如何使用

package controller

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

type UserInfo struct {
   Username string `"json:username"`
   Password string `"json:password"`
}

const TokenExpireDuration = time.Hour * 24
func authLoginHandler(c *gin.Context) {
	// 用户发送用户名和密码过来
	var user UserInfo
	err := c.ShouldBind(&user)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 2001,
			"msg":  "无效的参数",
		})
		return
	}
	// 校验用户名和密码是否正确 真实的是查数据库
	if user.Username == "jxb" && user.Password == "jxb123" {
		// 生成Token
		tokenString, _ := GenToken(user.Username)
                // 方式一:Redis/缓存**: 在Redis中存储带过期时间的token用于主动验证(理论代码)
                ctx := context.Background()
                err = rdb.Set(ctx, user.Username, tokenString, TokenExpireDuration).Err() 
                if err != nil { 
                    c.JSON(500, gin.H {
                      "code": 500,
                      "msg": "redis存token失败"
                    })
                    return
                }
                // 方式二:数据库: 存储在数据库表中,如果你需要跟踪活跃会话
                
		c.JSON(http.StatusOK, gin.H{
			"code": 2000,
			"msg":  "success",
			"data": gin.H{"token": tokenString},
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"code": 2002,
		"msg":  "鉴权失败",
	})
	return
}

// 用户登出时从Redis删除token 
func Logout(username string) error { 
    ctx := context.Background() 
    err := rdb.Del(ctx, username).Err() 
    // todo一堆其他操作
    if err != nil { 
        return err 
    } 
    return nil 
}

鉴权中间件的实现

// 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
		}
		// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
		mc, err := ParseToken(parts[1])
		if err != nil {
			c.JSON(http.StatusOK, gin.H{
				"code": 2005,
				"msg":  "无效的Token",
			})
			c.Abort()
			return
		}
		// 将当前请求的username信息保存到请求的上下文c上
		c.Set("username", mc.Username)
		c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
	}
}

可能会出现用户在操作的时候蓄操token的问题

    1. 怎么蓄token?
    • 重新设redis时长
    1. 什么时候蓄token?
    • 用户调用接口的时候(排除轮询接口,和websokit的接口),需要带token,和username的接口
    • 查过期时间是否小于1小时(或者某个特定时长),给蓄时间许多少根据要求来
func JWTAuthMiddlewareWithRefresh() gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"msg": "请求头中token不能为空"})
			c.Abort()
			return
		}

		tokenString := strings.TrimPrefix(authHeader, "Bearer ")

		// 首先解析token
		claims, err := ParseToken(tokenString)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"msg": "无效的token"})
			c.Abort()
			return
		}

		// 检查token是否即将过期(例如:剩余时间少于1小时)
		now := time.Now()
		expireTime := claims.ExpiresAt.Time
		timeLeft := expireTime.Sub(now)

		// 如果剩余时间少于1小时,则续期
		const refreshThreshold = time.Hour
		if timeLeft < refreshThreshold {
			// 生成新token
			newToken, err := GenToken(claims.Username)
			if err == nil {
				// 更新Redis中的token
				ctx := context.Background()
                                // 存新的token username: token键值对
				rdb.Set(ctx, claims.Username, newToken, 24 * time.Hour)

				// 在响应头中返回新token,前端接收到后重新存
				c.Header("X-Refreshed-Token", newToken)
			}
		}

		c.Set("username", claims.Username)
		c.Next()
	}
}

前端实现蓄token

// 请求拦截器 - 自动添加token
axios.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

// 响应拦截器 - 处理token续期
axios.interceptors.response.use(
  response => {
    // 检查是否有新的token 重点
    const newToken = response.headers['x-refreshed-token'];
    if (newToken) {
      // 更新本地存储的token
      localStorage.setItem('token', newToken);
      console.log('Token已自动续期');
    }
    return response;
  },
  error => {
    // 处理401等错误
    if (error.response?.status === 401) {
      // 清除本地token,跳转到登录页
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);