go语言jwt实践 | 青训营笔记

307 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天

所用依赖:

  • github.com/gin-gonic/gin
  • github.com/jinzhu/gorm
  • github.com/go-redis/redis
  • github.com/dgrijalva/jwt-go
  • golang.org/x/crypto/bcrypt

连接到MySQL和Redis数据库,设置Gin框架。定义两个路由,一个是登录路由,一个是需要身份验证的用户资料路由。路由函数之前定义一个authMiddleware中间件函数,以实现鉴权

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

var (
    db    *gorm.DB
    cache *redis.Client
)

func main() {
    // 连接数据库
    var err error
    db, err = gorm.Open("mysql", "root:@tcp(127.0.0.1:3306)/users?charset=utf8mb4&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // 连接 Redis
    cache = redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    // 设置 Gin
    r := gin.Default()

    // 注册路由
    r.POST("/login", loginHandler)
    r.GET("/profile", authMiddleware(), profileHandler)

    // 启动服务器
    if err := r.Run(":8080"); err != nil {
        panic(err)
    }
}

这个处理程序将接收一个JSON请求体,其中包含usernamepassword字段。检查这些凭据是否与数据库中的任何记录匹配。如果是,将生成一个JWT令牌,并将其存储在Redis中。使用generateToken函数来生成JWT令牌。

package handlers
import (
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis"
    "github.com/jinzhu/gorm"
    "github.com/dgrijalva/jwt-go"
    "example.com/auth-example/models"
)

type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type LoginResponse struct {
    Token string `json:"token"`
}

func loginHandler(c *gin.Context) {
    var req LoginRequest
    if err := c.BindJSON(&req); err != nil {
        c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    var user models.User
    if err := db.Where("username = ?", req.Username).First(&user).Error; err != nil {
        if err == gorm.ErrRecordNotFound {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
            return
        }
        c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    if !user.CheckPassword(req.Password) {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
        return
    }

    token, err := generateToken(user.ID)
    if err != nil {
        c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    cache.Set(token, user.ID, 0)

    c.JSON(http.StatusOK, LoginResponse{Token: token})
}

func generateToken(userID uint) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "user_id": userID,
    })

    return token.SignedString([]byte("secret"))
}

中间件函数

package middlewares

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis"
    "github.com/dgrijalva/jwt
type AuthenticatedUser struct {
ID uint
Username string
}

func authMiddleware(cache *redis.Client) gin.HandlerFunc {
    return func(c *gin.Context) {
    tokenHeader := c.GetHeader("Authorization")
    if tokenHeader == "" {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
    return
}
    tokenParts := strings.Split(tokenHeader, " ")
    if len(tokenParts) != 2 || strings.ToLower(tokenParts[0]) != "bearer" {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"})
        return
    }

    tokenString := tokenParts[1]

    claims := jwt.MapClaims{}
    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        return []byte("secret"), nil
    })

    if err != nil {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
        return
    }

    if !token.Valid {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
        return
    }

    userID := uint(claims["user_id"].(float64))
    cachedID, err := cache.Get(tokenString).Uint64()
    if err != nil || cachedID != uint64(userID) {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
        return
    }

    c.Set("user", AuthenticatedUser{ID: userID})
    c.Next()
}
}
这个中间件函数将检查请求头中的授权标头,并将其解析为JWT令牌。如果令牌无效,中间件将中止请求并返回`401 Unauthorized`响应。否则,从令牌中提取用户ID,并检查该令牌是否存在于Redis缓存中,如果是,继续处理请求。