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

golang,gin,jwt · 浏览次数 : 0

小编点评

本文介绍了JWT(JSON Web Token)的基本概念、工作流程以及如何使用JWT实现安全的API认证。 **JWT简介**: JWT是一种开放标准,用于安全地在双方之间传递信息,尤其在身份验证和授权场景中广泛应用。它由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部包含JWT的元数据,如类型和使用的签名算法;载荷包含声明,即用户的相关信息;签名是通过头部和载荷使用指定算法计算得出的。 **工作流程**: 1. 用户向服务器提供凭证(如用户名和密码)。 2. 服务器验证凭证无误后,生成一个JWT,并使用秘钥对其进行签名。 3. 客户端收到JWT后,在后续请求中自动将其发送给服务器以证明用户身份。 4. 服务器收到JWT后,使用相同的秘钥验证JWT的签名,确保其未被篡改,并检查过期时间等其他声明。 5. 验证通过后,服务器允许执行请求。 **优势与注意事项**: JWT的无状态特性减轻了服务器压力,同时便于跨域认证。但安全性依赖于秘钥的安全保管和对JWT过期时间等的合理设置。 **示例代码**: 文章提供了完整的示例代码目录结构,包括客户端测试脚本、控制器、模型、中间件和工具类包等。 **总结**: 本文通过一个简单的gin-jwt应用展示了如何使用JWT实现API认证。通过JWT的机制,实现了用户身份的有效验证和授权,同时保证了API的安全性。

正文

什么是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()
	}
}

参考

与[golang]在Gin框架中使用JWT鉴权相似的内容:

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

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

摸鱼快报:golang net/http中的雕虫小技

以后会开一个板块,摸鱼快报,快速记录这几周开发中雕虫小技。 1. 向开发环境localhost:3000种植cookie 前端使用Create React App脚手架,默认以localhost:3000端口启动; 后端使用golang-gin框架,使用8034端口启动。 登录模块走的是sso,前后

golang trace view 视图详解

> 大家好,我是蓝胖子,在golang中可以使用go pprof的工具对golang程序进行性能分析,其中通过go trace 命令生成的trace view视图对于我们分析系统延迟十分有帮助,鉴于当前对trace view视图的介绍还是很少,在粗略的看过trace统计原理后,我将对这部分做比较详细

golang pprof 监控系列(4) —— goroutine thread 统计原理

golang pprof 监控系列(4) —— goroutine thread 统计原理 大家好,我是蓝胖子。 在之前 golang pprof监控 系列文章里我分别介绍了go trace以及go pprof工具对memory,block,mutex这些维度的统计原理,今天我们接着来介绍golan

Go pprof 认知到实践

快速开始 测试环境:go version go1.22.2 windows/amd64,源代码开源在 https://github.com/oldme-git/teach-study/tree/master/golang/base/pprof 在正式开始之前,请确保安装 graphviz,这一步不可

golang uuid库介绍

简介: 在现代软件开发中,全球唯一标识符(UUID)在许多场景中发挥着重要的作用。UUID是一种128位的唯一标识符,它能够保证在全球范围内不重复。在Go语言中,我们可以使用第三方库`github.com/google/uuid`来方便地生成UUID。本文将介绍如何使用这个库来生成不同版本的UUID

Golang漏洞管理

原文在[这里](https://go.dev/security/vuln/) ## 概述 Go帮助开发人员检测、评估和解决可能被攻击者利用的错误或弱点。在幕后,Go团队运行一个管道来整理关于漏洞的报告,这些报告存储在Go漏洞数据库中。各种库和工具可以读取和分析这些报告,以了解特定用户项目可能受到的影

golang interface 和 struct 添加方法的区别

在 Go 语言中,struct 和 interface 都可以关联方法,但它们的方式不同: 1. struct 添加方法: 结构体(struct)本身不直接包含方法,但可以通过定义一个指向该结构体类型的指针作为接收者的函数来为结构体“添加”方法。 type MyStruct struct { //

golang 的 net/http 和 net/rpc 的区别, rpc 效率比 http 高?

在Go语言中,net/http 和 net/rpc 是两个不同的包,它们分别用于实现不同的网络通信模式: net/http: net/http 包主要用于构建Web服务和客户端,它实现了HTTP协议,这是互联网上最广泛使用的应用层协议之一,主要用于传输超文本(HTML)和其他资源。 HTTP 协议是

golang 泛型的格式写法

Go语言中的泛型(Generics)是在 Go 1.18 版本中引入的一个重要特性,它允许你编写可重用的代码,而不需要为每种数据类型重复编写相同的逻辑。 泛型通过参数化类型(type parameters)来实现,使得函数、方法、接口和结构体可以与多种类型一起工作。 下面详细介绍Go语言中泛型的基本