GO-JWT

什么是 JWT

JWT,全称 JSON Web Token,是一种开放标准(RFC 7519),用于安全地在双方之间传递信息。尤其适用于身份验证和授权场景。JWT 的设计允许信息在各方之间安全地、 compactly(紧凑地)传输,因为其自身包含了所有需要的认证信息,从而减少了需要查询数据库或会话存储的需求。

JWT主要由三部分组成,通过.连接:

  1. Header(头部):描述JWT的元数据,通常包括类型(通常是JWT)和使用的签名算法(如HS256RS256等)。
  2. Payload(载荷):包含声明(claims),即用户的相关信息。这些信息可以是公开的,也可以是私有的,但应避免放入敏感信息,因为该部分可以被解码查看。载荷中的声明可以验证,但不加密。
  3. Signature(签名):用于验证JWT的完整性和来源。它是通过将Header和Payload分别进行Base64编码后,再与一个秘钥(secret)一起通过指定的算法(如HMAC SHA256)计算得出的。

JWT的工作流程大致如下:

  • 认证阶段:用户向服务器提供凭证(如用户名和密码)。服务器验证凭证无误后,生成一个JWT,其中包含用户标识符和其他声明,并使用秘钥对其进行签名。
  • 使用阶段:客户端收到JWT后,可以在后续的每个请求中将其放在HTTP请求头中发送给服务器,以此证明自己的身份。
  • 验证阶段:服务器收到JWT后,会使用相同的秘钥验证JWT的签名,确保其未被篡改,并检查过期时间等其他声明,从而决定是否允许执行请求。

JWT的优势在于它的无状态性,服务器不需要存储会话信息,这减轻了服务器的压力,同时也方便了跨域认证。但需要注意的是,JWT的安全性依赖于秘钥的安全保管以及对JWT过期时间等的合理设置。

注册的时候 加密 密码

// user.go
func (u *User) SaveUser() (*User, error) {
	err := DB.Create(&u).Error
	if err != nil {
		return &User{}, err
	}
	return u, nil
}

// 使用gorm的hook在保存密码前对密码进行hash
func (u *User) BeforeSave(tx *gorm.DB) error {
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	u.Password = string(hashedPassword)
	u.Username = html.EscapeString(strings.TrimSpace(u.Username))
	return nil
}


// register.go
func Register(c *gin.Context) {
    var req ReqRegister

	if err := c.ShouldBindBodyWithJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"data": err.Error(),
		})
		return
	}

	u := models.User{
		Username: req.Username,
		Password: req.Password,
	}

	_, err := u.SaveUser()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"data": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"message": "register success",
		"data":    req,
	})
}

钩子事件:可在创建用户前后执行的事件。

Login

//controllers/login.go
func Login(c *gin.Context) {
	var req ReqLogin
	if err := c.ShouldBindBodyWithJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	u := models.User{
		Username: req.Username,
		Password: req.Password,
	}

	// 调用 models.LoginCheck 对用户名和密码进行验证
	token, err := models.LoginCheck(u.Username, u.Password)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "username or password is incorrect.",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"token": token,
	})
}

//models/user.go
func VerifyPassword(password, hashedPassword string) error {
	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}


func LoginCheck(username, password string) (string, error) {
	var err error
	u := User{}

	err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error
	if err != nil {
		return "", err
	}
	err = VerifyPassword(password, u.Password)
	if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
		return "", err
	}

	token, err := token.GenerateToken(u.ID)
	if err != nil {
		return "", err
	}
	return token, nil
}

//utils/token.go
package utils

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
)

func GenerateToken(user_id uint) (string, error) {
	token_lifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
	if err != nil {
		return "", err
	}

	claims := jwt.MapClaims{}
	claims["authorized"] = true
	claims["user_id"] = user_id
	claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	return token.SignedString([]byte(os.Getenv("API_SECRET")))
}

func TokenValid(c *gin.Context) error {
	tokenString := ExtractToken(c)
	fmt.Println(tokenString)
	_, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("API_SECRET")), nil
	})
	if err != nil {
		return err
	}

	return nil
}

// 从请求头中获取token
func ExtractToken(c *gin.Context) string {
	bearerToken := c.GetHeader("Authorization")
	if len(strings.Split(bearerToken, " ")) == 2 {
		return strings.Split(bearerToken, " ")[1]
	}
	return ""
}

// 从jwt中解析出user_id
func ExtractTokenID(c *gin.Context) (uint, error) {
	tokenString := ExtractToken(c)
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("API_SECRET")), nil
	})
	if err != nil {
		return 0, err
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	// 如果jwt有效,将user_id转换为浮点数字符串,然后再转换为 uint32
	if ok && token.Valid {
		uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)
		if err != nil {
			return 0, err
		}
		return uint(uid), nil
	}

	return 0, nil
}

设置环境变量:

go get github.com/joho/godotenv

.env:
TOKEN_HOUR_LIFESPAN=1
API_SECRET="wP3-sN6&gG4-lV8>gJ9)"

创建 JWT 认证中间件

//middlewares/jwt.go
package middlewares

import (
	"UserLoginSystem/utils"
	"net/http"

	"github.com/gin-gonic/gin"
)

func JwtAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		err := utils.TokenValid(c)
		if err != nil {
			c.String(http.StatusUnauthorized, err.Error())
			c.Abort()
			return
		}
		c.Next()
	}
}

//controllers/login.go
func CurrentUser(c *gin.Context) {
	// 从token中解析出user_id
	user_id, err := token.ExtractTokenID(c)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}

	// 根据user_id从数据库查询数据
	u, err := models.GetUserByID(user_id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"message": "success",
		"data": u,
	})
}

//models/user.go

总结

1. JWT 核心结构

// Header示例(实际存储为Base64编码)
// {
//   "alg": "HS256",  // 必填,签名算法类型
//   "typ": "JWT"     // 必填,令牌类型固定值
// }

// Payload示例(实际存储为Base64编码)
// {
//   "user_id": 123,   // 自定义声明
//   "exp": 1672531200 // 标准声明-过期时间(UNIX时间戳)
// }

// Signature生成公式:
// HMACSHA256(
//   base64UrlEncode(header) + "." + 
//   base64UrlEncode(payload),
//   secretKey
// )

2. 密码安全处理

// models/user.go

// BeforeSave - Gorm的Hook函数,在保存前自动执行
func (u *User) BeforeSave(tx *gorm.DB) error {
    // bcrypt加密(自动加盐,DefaultCost=10)
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err // 加密失败阻止保存
    }
    u.Password = string(hashedPassword)
    
    // 用户名安全处理
    u.Username = html.EscapeString(strings.TrimSpace(u.Username))
    return nil
}

3. 登录与Token生成

// controllers/login.go
func Login(c *gin.Context) {
    // 绑定JSON数据到结构体
    if err := c.ShouldBindBodyWithJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": "请求格式错误"})
        return
    }
    
    // 生成Token(包含用户ID和过期时间)
    token, err := models.LoginCheck(req.Username, req.Password)
    // ...错误处理
}

// models/user.go
func LoginCheck(username, password string) (string, error) {
    // 数据库查询用户
    err = DB.Where("username = ?", username).Take(&u).Error
    
    // bcrypt密码验证
    err = VerifyPassword(password, u.Password)
    
    // 生成JWT Token
    token, err := token.GenerateToken(u.ID)
    return token, nil
}

4. Token工具函数

// utils/token.go

// GenerateToken - 生成JWT Token
func GenerateToken(user_id uint) (string, error) {
    // 从环境变量读取有效期(小时)
    lifespan, _ := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
    
    claims := jwt.MapClaims{
        "user_id": user_id,                    // 用户标识
        "exp": time.Now().Add(time.Hour * lifespan).Unix(), // 过期时间
    }
    
    // 创建Token(使用HS256算法)
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    
    // 签名(使用环境变量中的秘钥)
    return token.SignedString([]byte(os.Getenv("API_SECRET")))
}

// TokenValid - 验证Token有效性
func TokenValid(c *gin.Context) error {
    tokenString := ExtractToken(c) // 从Header提取
    
    // 解析并验证签名算法和秘钥
    _, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
        // 检查算法是否为HMAC
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("不支持的签名算法")
        }
        return []byte(os.Getenv("API_SECRET")), nil
    })
    return err // nil表示验证通过
}

5. 中间件实现

// middlewares/jwt.go

// JwtAuthMiddleware - JWT认证中间件
func JwtAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 验证Token
        if err := utils.TokenValid(c); err != nil {
            c.AbortWithStatus(401) // 认证失败
            return
        }
        c.Next() // 继续后续处理
    }
}

6. 环境变量配置

# .env文件
TOKEN_HOUR_LIFESPAN=1    # Token有效期(小时)
API_SECRET="your_strong_secret"  # 签名秘钥(建议32+字符)

关键安全注意事项

  1. 秘钥管理

    • API_SECRET 必须足够复杂(建议随机生成32位以上字符串)
    • 生产环境必须通过环境变量注入,禁止硬编码
  2. Token有效期

    • 根据业务需求设置合理有效期(通常1-24小时)
    • 敏感操作应使用更短的有效期
  3. 密码存储

    • 必须使用bcrypt等自适应哈希算法
    • 禁止使用MD5/SHA1等快速哈希
  4. 传输安全

    • 必须通过HTTPS传输
    • 建议设置Secure和HttpOnly的Cookie
posted @ 2025-04-01 23:05  XiaoMo247  阅读(105)  评论(0)    收藏  举报