[golang]在Gin框架中使用JWT鉴权

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: [golang]在Gin框架中使用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过期时间等的合理设置。

API设计

这里设计两个公共接口和一个受保护的接口。

API 描述
/api/login 公开接口。用于用户登录
/api/register 公开接口。用于用户注册
/api/admin/user 保护接口,需要验证JWT

开发准备

初始化项目目录并切换进入

mkdir gin-jwt
cd gin-jwt

使用go mod初始化工程

go mod init gin-jwt

安装依赖

go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/golang-jwt/jwt/v5
go get -u github.com/joho/godotenv
go get -u golang.org/x/crypto

创建第一个API

一开始我们可以在项目的根目录中创建文件main.go

touch main.go

添加以下内容

package main
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
func main() {
    r := gin.Default()
    public := r.Group("/api")
    {
        public.POST("/register", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "data": "test. register api",
            })
        })
    }
    r.Run("0.0.0.0:8000")
}

测试运行

go run main.go

客户端测试。正常的话会有以下输出

$ curl -X POST http://127.0.0.1:8000/api/register
{"data":"test. register api"}

完善register接口

现在register接口已经准备好了,但一般来说我们会把接口业务逻辑放在单独的文件中,而不是和接口定义写在一块。

创建一个控制器的包目录,并添加文件

mkdir controllers
touch controllers/auth.go

auth.go文件内容

package controllers
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
func Register(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "data": "hello, this is register endpoint",
    })
}

更新main.go文件

package main
import (
    "github.com/gin-gonic/gin"
    "gin-jwt/controllers"
)
func main() {
    r := gin.Default()
    public := r.Group("/api")
    {
        public.POST("/register", controllers.Register)
    }
    r.Run("0.0.0.0:8000")
}

重新运行测试

go run main.go

客户端测试

$ curl -X POST http://127.0.0.1:8000/api/register
{"data":"hello, this is register endpoint"}

解析register的客户端请求

客户端请求register api需要携带用户名和密码的参数,服务端对此做解析。编辑文件controllers/auth.go

package controllers
import (
    "net/http"
    "github.com/gin-gonic/gin"
)
// /api/register的请求体
type ReqRegister struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}
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
    }
    c.JSON(http.StatusOK, gin.H{
        "data": req,
    })
}

客户端请求测试

$ curl -X POST http://127.0.0.1:8000/api/register -d '{"username": "zhangsan", "password": "123456"}' -H 'Content-Type=application/json'
{"data":{"username":"zhangsan","password":"123456"}}

连接关系型数据库

一般会将数据保存到专门的数据库中,这里用PostgreSQL来存储数据。Postgres使用docker来安装。安装完postgres后,创建用户和数据库:

create user ginjwt encrypted password 'ginjwt';
create database ginjwt owner = ginjwt;

创建目录models,这个目录将包含连接数据库和数据模型的代码。

mkdir models

编辑文件models/setup.go

package models
import (
    "fmt"
    "log"
    "os"
    "github.com/joho/godotenv"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
    err := godotenv.Load(".env")
    if err != nil {
        log.Fatalf("Error loading .env file. %v\n", err)
    }
    // DbDriver := os.Getenv("DB_DRIVER")
    DbHost := os.Getenv("DB_HOST")
    DbPort := os.Getenv("DB_PORT")
    DbUser := os.Getenv("DB_USER")
    DbPass := os.Getenv("DB_PASS")
    DbName := os.Getenv("DB_NAME")
    dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai password=%s", DbHost, DbPort, DbUser, DbName, DbPass)
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Connect to database failed, %v\n", err)
    } else {
        log.Printf("Connect to database success, host: %s, port: %s, user: %s, dbname: %s\n", DbHost, DbPort, DbUser, DbName)
    }
    // 迁移数据表
    DB.AutoMigrate(&User{})
}

新建并编辑环境配置文件.env

DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=ginjwt
DB_PASS=ginjwt
DB_NAME=ginjwt

创建用户模型,编辑代码文件models/user.go

package models
import (
    "html"
    "strings"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)
type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"password"`
}
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
}

更新main.go

package main
import (
    "github.com/gin-gonic/gin"
    "gin-jwt/controllers"
    "gin-jwt/models"
)
func init() {
    models.ConnectDatabase()
}
func main() {
    r := gin.Default()
    public := r.Group("/api")
    {
        public.POST("/register", controllers.Register)
    }
    r.Run("0.0.0.0:8000")
}

更新controllers/auth.go

package controllers
import (
    "net/http"
    "gin-jwt/models"
    "github.com/gin-gonic/gin"
)
// /api/register的请求体
type ReqRegister struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}
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,
    })
}

重新运行服务端后,客户端测试

$ curl -X POST http://127.0.0.1:8000/api/register -d '{"username": "zhangsan", "password": "123456"}' -H 'Content-Type=application/json'
{"data":{"username":"zhangsan","password":"123456"},"message":"register success"}

添加login接口

登录接口实现的也非常简单,只需要提供用户名和密码参数。服务端接收到客户端的请求后到数据库中去匹配,确认用户是否存在和密码是否正确。如果验证通过则返回一个token,否则返回异常响应。

首先在main.go中注册API

// xxx
func main() {
    // xxx
    r := gin.Default()
    public := r.Group("/api")
    {
        public.POST("/register", controllers.Register)
        public.POST("/login", controllers.Login)
    }
}

auth.go中添加Login控制器函数

// api/login 的请求体
type ReqLogin struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}
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,
    })
}

LoginCheck方法在models/user.go文件中实现

package models
import (
    "gin-jwt/utils/token"
    "html"
    "strings"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)
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
}

这里将token相关的函数放到了单独的模块中,新增相关目录并编辑文件

mkdir -p utils/token
touch utils/token/token.go

以下代码为token.go的内容,包含的几个函数在后面会用到

package token
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
}

.env文件中添加两个环境变量的配置。TOKEN_HOUR_LIFESPAN设置token的过期时长,API_SECRET是jwt的密钥。

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

测试,这里改用python代码进行测试

import requests
import json
headers = {
    "Content-Type": "application/json",
}
resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)
def register(username: str, password: str):
    req_body = {
        "username": username,
        "password": password,
    }
    resp = requests.post("http://127.0.0.1:8000/api/register", data=json.dumps(req_body), headers=headers)
    print(resp.text)
def login(username: str, password: str):
    req_body = {
        "username": username,
        "password": password,
    }
    resp = requests.post("http://127.0.0.1:8000/api/login", data=json.dumps(req_body), headers=headers)
    print(resp.text)
    if resp.status_code == 200:
        return resp.json()["token"]
    else:
        return ""
if __name__ == "__main__":
    username = "lisi"
    password = "123456"
    register(username, password)
    token = login(username, password)
    print(token)

创建JWT认证中间件

创建中间件目录和代码文件

mkdir middlewares
touch middlewares/middlewares.go

内容如下

package middlewares
import (
    "gin-jwt/utils/token"
    "net/http"
    "github.com/gin-gonic/gin"
)
func JwtAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        err := token.TokenValid(c)
        if err != nil {
            c.String(http.StatusUnauthorized, err.Error())
            c.Abort()
            return
        }
        c.Next()
    }
}

main.go文件中注册路由的时候使用中间件

func main() {
    models.ConnectDatabase()
    r := gin.Default()
    public := r.Group("/api")
    {
        public.POST("/register", controllers.Register)
        public.POST("/login", controllers.Login)
    }
    protected := r.Group("/api/admin")
    {
        protected.Use(middlewares.JwtAuthMiddleware())
        protected.GET("/user", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "status":  "success",
                "message": "authorized",
            })
        })
    }
    r.Run("0.0.0.0:8000")
}

controllers/auth.go文件中实现CurrentUser

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文件中实现GetUserByID

// 返回前将用户密码置空
func (u *User) PrepareGive() {
    u.Password = ""
}
func GetUserByID(uid uint) (User, error) {
    var u User
    if err := DB.First(&u, uid).Error; err != nil {
        return u, errors.New("user not found")
    }
    u.PrepareGive()
    return u, nil
}

至此,一个简单的gin-jwt应用就完成了。

客户端测试python脚本

服务端的三个接口这里用python脚本来测试

import requests
import json
headers = {
    # "Authorization": f"Bearer {token}",
    "Content-Type": "application/json",
}
resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)
def register(username: str, password: str):
    req_body = {
        "username": username,
        "password": password,
    }
    resp = requests.post("http://127.0.0.1:8000/api/register", data=json.dumps(req_body), headers=headers)
    print(resp.text)
def login(username: str, password: str):
    req_body = {
        "username": username,
        "password": password,
    }
    resp = requests.post("http://127.0.0.1:8000/api/login", data=json.dumps(req_body), headers=headers)
    print(resp.text)
    if resp.status_code == 200:
        return resp.json()["token"]
    else:
        return ""
def test_protect_api(token: str):
    global headers
    headers["Authorization"] = f"Bearer {token}"
    resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)
    print(resp.text)
if __name__ == "__main__":
    username = "lisi"
    password = "123456"
    register(username, password)
    token = login(username, password)
    test_protect_api(token)

运行脚本结果

{"message":"register success"}
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MTk5NDA0NjAsInVzZXJfaWQiOjZ9.qkzn0Ot9hAb54l3RFbGUohHJ9oezGia5x_oXppbD2jQ"}
{"data":{"ID":6,"CreatedAt":"2024-07-03T00:14:20.187725+08:00","UpdatedAt":"2024-07-03T00:14:20.187725+08:00","DeletedAt":null,"username":"wangwu","password":""},"message":"success"}

完整示例代码

目录结构

├── client.py  # 客户端测试脚本
├── controllers  # 控制器相关包
│   └── auth.go  # 控制器方法实现
├── gin-jwt.bin  # 编译的二进制文件
├── go.mod  # go 项目文件
├── go.sum  # go 项目文件
├── main.go  # 程序入口文件
├── middlewares  # 中间件相关包
│   └── middlewares.go  # 中间件代码文件
├── models  # 存储层相关包
│   ├── setup.go  # 配置数据库连接
│   └── user.go  # user模块相关数据交互的代码文件
├── README.md  # git repo的描述文件
└── utils  # 工具类包
    └── token  # token相关工具类包
        └── token.go  # token工具的代码文件

main.go

package main
import (
    "log"
    "github.com/gin-gonic/gin"
    "gin-jwt/controllers"
    "gin-jwt/middlewares"
    "gin-jwt/models"
    "github.com/joho/godotenv"
)
func init() {
    err := godotenv.Load(".env")
    if err != nil {
        log.Fatalf("Error loading .env file. %v\n", err)
    }
}
func main() {
    models.ConnectDatabase()
    r := gin.Default()
    public := r.Group("/api")
    {
        public.POST("/register", controllers.Register)
        public.POST("/login", controllers.Login)
    }
    protected := r.Group("/api/admin")
    {
        protected.Use(middlewares.JwtAuthMiddleware()) // 在路由组中使用中间件
        protected.GET("/user", controllers.CurrentUser)
    }
    r.Run("0.0.0.0:8000")
}

controllers

  • auth.go
package controllers
import (
    "net/http"
    "gin-jwt/models"
    "gin-jwt/utils/token"
    "github.com/gin-gonic/gin"
)
// /api/register的请求体
type ReqRegister struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}
// api/login 的请求体
type ReqLogin struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}
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,
    })
}
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",
    })
}
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

  • setup.go
package models
import (
    "fmt"
    "log"
    "os"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
    var err error
    DbHost := os.Getenv("DB_HOST")
    DbPort := os.Getenv("DB_PORT")
    DbUser := os.Getenv("DB_USER")
    DbPass := os.Getenv("DB_PASS")
    DbName := os.Getenv("DB_NAME")
    dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai password=%s", DbHost, DbPort, DbUser, DbName, DbPass)
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Connect to database failed, %v\n", err)
    } else {
        log.Printf("Connect to database success, host: %s, port: %s, user: %s, dbname: %s\n", DbHost, DbPort, DbUser, DbName)
    }
    // 迁移数据表
    DB.AutoMigrate(&User{})
}
  • user.go
package models
import (
    "errors"
    "gin-jwt/utils/token"
    "html"
    "strings"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)
type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"password"`
}
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
}
// 返回前将用户密码置空
func (u *User) PrepareGive() {
    u.Password = ""
}
// 对哈希加密的密码进行比对校验
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
}
func GetUserByID(uid uint) (User, error) {
    var u User
    if err := DB.First(&u, uid).Error; err != nil {
        return u, errors.New("user not found")
    }
    u.PrepareGive()
    return u, nil
}

utils

  • token/token.go
package token
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
}

middlewares

  • middlewares.go
package middlewares
import (
    "gin-jwt/utils/token"
    "net/http"
    "github.com/gin-gonic/gin"
)
func JwtAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        err := token.TokenValid(c)
        if err != nil {
            c.String(http.StatusUnauthorized, err.Error())
            c.Abort()
            return
        }
        c.Next()
    }
}

参考

相关文章
|
3天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
14 1
|
5月前
|
负载均衡 监控 Go
使用Golang框架构建分布式系统
本文探讨了使用Golang构建分布式系统的方法。Golang因其高效、简洁的语法和并发支持成为理想的开发语言。文中列举了几个常用的Golang框架,如Echo、Gin、gRPC和NATS等,并强调了服务拆分、通信机制、负载均衡等构建分布式系统的关键要素。通过选择合适的框架,遵循需求分析、技术选型、服务设计等步骤,开发者可以构建出高性能、高可用和可扩展的系统。此外,文中还提供了一个使用gRPC和etcd的简单代码案例来说明实现过程。
258 4
|
3月前
|
存储 开发框架 算法
ASP.NET Core 标识(Identity)框架系列(四):闲聊 JWT 的缺点,和一些解决思路
ASP.NET Core 标识(Identity)框架系列(四):闲聊 JWT 的缺点,和一些解决思路
|
3月前
|
JSON 数据安全/隐私保护 数据格式
Nest.js 实战 (八):基于 JWT 的路由身份认证鉴权
这篇文章介绍了身份验证的重要性和多种处理策略,重点放在了JWT(JSON Web Token)认证在Nest.js框架中的应用。文章包含了JWT认证的流程,如何在Nest.js中实现,以及如何创建JWT认证策略。包括了安装依赖,创建处理认证流程的文件,以及如何使用HttpException过滤器来处理未登录访问。
190 0
Nest.js 实战 (八):基于 JWT 的路由身份认证鉴权
|
3月前
|
存储 开发框架 JSON
ASP.NET Core 标识(Identity)框架系列(二):使用标识(Identity)框架生成 JWT Token
ASP.NET Core 标识(Identity)框架系列(二):使用标识(Identity)框架生成 JWT Token
|
3月前
|
JSON 人工智能 算法
Golang 搭建 WebSocket 应用(四) - jwt 认证
Golang 搭建 WebSocket 应用(四) - jwt 认证
54 0
|
3月前
|
NoSQL 安全 Java
Java Spring Boot中使用Shiro、JWT和Redis实现用户登录鉴权
Java Spring Boot中使用Shiro、JWT和Redis实现用户登录鉴权
|
3月前
|
网络协议 Go
[golang]gin框架接收websocket通信
[golang]gin框架接收websocket通信
100 0
|
3月前
|
Go
[golang]jwt生成与解析
[golang]jwt生成与解析
164 0