• [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()
    	}
    }
    

    参考

  • 相关阅读:
    将vscode变成C++ IDE
    手把手教你使用PLSQL远程连接Oracle数据库【内网穿透】
    手机和windows的便签怎么共享账号使用
    SpringBoot如何配置拦截器呢?
    ArrayList 源码分析扩容原理
    素数算法(Prime Num Algorithm)
    Java的abstract应用和代理模式应用
    jmeter 性能测试工具的使用(Web性能测试)
    【JavaSE】Java方法
    【爬虫系列】Python 爬虫入门(1)
  • 原文地址:https://www.cnblogs.com/XY-Heruo/p/18280787