2024年7月

什么是JWT

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

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

  1. Header(头部)
    :描述JWT的元数据,通常包括类型(通常是
    JWT
    )和使用的签名算法(如
    HS256

    RS256
    等)。
  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()
	}
}

参考

本文分享自华为云社区
《Sermant自定义插件开发上手体验》
,作者:华为云开源。

一、研究缘由

由于目前我们所处的行业是汽车行业,项目上进行云服务的迁移时使用到了
Sermant
中的相关插件, 为了加深对Sermant开发和运行机制的了解,我们从零开始体验Sermant自定义插件的开发。

下面我们就Sermant-example中的first-plugin-demo来进行研究说明。

二、下载Sermant-example

首先我们下载sermant-example的demo:

下载完成之后,我们从最简单的自定义插件开始,也即first-plugin-demo。

三、执行打包

对first-plugin-demo执行打包,打完包的结构:

可以看到我们的项目和对应的插件模板项目都在里面了。

四、启动项目

java -javaagent:sermant-agent.jar -jar Application.jar

然后访问controller方法

从而可以看到拦截的效果:

可以看到启动的过程中,完成了拦截的效果。

也即它走了拦截器的前置和后置方法。

五、动态配置验证

配置的动态配置里面配置的配置中心是zookeeper,因此我们在启动项目前需要启动一个zookeeper作为配置中心,同时开启动态配置开关。

完成之后,我们需要在相应的监听节点下创建一个配置项,来测试动态配置的功能:

再次进行访问,可以在控制台看到如下效果:

也即完成了动态配置的功能。而我们可以看到

其实质是创建了配置监听器,实现了配置处理的process方法,其中DynamicConfigEvent就是监听到的配置更改的事件,包含了配置的group,key,content等配置信息。实现动态配置的相关实现是调用了自己实现的process方法,基于map对配置进行存储和处理,从而实现配置切换。

六、全流程中的参数argsMap到底是什么

之前一直很好奇配置里面的信息到底是什么,最近debug之后有所发现。

我们可以看到加载的argMap里面的相关参数:

我们可以看到argsMap里面的参数基本上和上面的配置,同时会加载插件里面的配置信息。完成这些操作之后,就可以在全流程中去完成对应信息的加载了。

ConfigManager.initialize(argsMap) ,主要是解析一些配置,由于配置存在yaml和properties等不同的形式,因此这里采用策略模式来进行解析。主要的相关配置信息可以参考BaseConfig这个接口的实现。

七、插件的加载是在plugins.yaml

插件的加载是在plugins.yaml里面配置的,比如我们自定义的插件:

而实现插件的关键在于plugins.yaml中配置了哪些插件。因为这些插件正是后续进行拦截的基础。也即它告诉了程序,需要加载哪些插件,不需要加载哪些插件。有了这个基础,才会进行后面精准的转换、installOn操作。

八、拦截原理

我们可以看到上面控制台打印的拦截信息,那如何实现拦截的呢?

可以看到我们执行first-plugin-demo这个示例的时候:会发现我们执行业务方法的时候,它就会进行织入拦截器,执行对应的onMethodEnter和方法执行完后的onMethodExit方法。

这两个方法和byte-buddy中的使用实现的功能是类似的。在需要拦截的方法中执行织入的逻辑。

同时可以从控制台debug中的信息可以看到调用的情况:

完成上面的调用之后,我们便可以看到控制台输出的信息了。

这里我们以进入方法为例,来进行说明:

可以看到进入对应的织入方法之后,最终会走到我们需要进行扩展的迭代器中,来实现迭代。

可以看到进入了first-plugin-demo中的插件拦截:

完成拦截进入到业务方法中,实现拦截功能的织入。然后进入后置拦截,完成after的拦截逻辑。

参考:

官网:
https://sermant.io

仓库地址:
https://github.com/sermant-io/Sermant

Demo仓库:
https://github.com/sermant-io/Sermant-examples

点击关注,第一时间了解华为云新鲜技术~

0、停止使用该索引的服务(避免新加了数据没备份)

1、备份filesearch索引(检查备份的索引和原索引数据条数是否一致)

1 POST    http://127.0.0.1:9200/_reindex
2 {3     "source":{4         "index":"filesearch"
5 },6     "dest":{7         "index":"filesearch_bak"
8 }9 }

2、删除filesearch索引

3、新建索引(不包括不区分大小写的字段fileName)

1 PUT    http://127.0.0.1:9200/filesearch
2 {3     "mappings": {4         "properties": {5             "extendName": {6                 "type": "text",7                 "fields": {8                     "keyword": {9                         "ignore_above": 256,10                         "type": "keyword"
11 }12 }13 },14             "filePath": {15                 "type": "text",16                 "fields": {17                     "keyword": {18                         "ignore_above": 256,19                         "type": "keyword"
20 }21 }22 },23             "fileId": {24                 "type": "text",25                 "fields": {26                     "keyword": {27                         "ignore_above": 256,28                         "type": "keyword"
29 }30 }31 },32             "isDir": {33                 "type": "long"
34 }35 }36 }37 }

4、关闭索引

1 POST    http://127.0.0.1:9200/filesearch/_close

5、修改索引属性(添加不区分大小写的属性)

1 PUT    http://127.0.0.1:9200/filesearch/_settings
2 {3   "analysis": {4     "normalizer": {5       "lowercase_normalizer": {6         "type": "custom",7         "char_filter": [],8         "filter": [9           "lowercase"
10 ]11 }12 }13 }14 }

6、更新映射(添加不区分大小写的字段fileName)

1 PUT    http://127.0.0.1:9200/filesearch/_mapping
2 {3     "properties": {4         "fileName": {5             "type": "text",6             "fields": {7                 "keyword": {8                     "normalizer": "lowercase_normalizer",9                     "ignore_above": 256,10                     "type": "keyword"
11 }12 }13 }14 }15 }

7、开启索引

1 POST    http://127.0.0.1:9200/filesearch/_open

8、复制备份索引到新索引

1 POST    http://127.0.0.1:9200/_reindex
2 {3     "source":{4         "index":"filesearch_bak"
5 },6     "dest":{7         "index":"filesearch"
8 }9 }

9、启动使用该索引的服务

完成!

测试:

1、写入数据

 1 PUT    http://127.0.0.1:9200/ws_index/_create/1/
 2 {
3 "fileId": "83f311d71e5d4d799e5b254cf9305b04",
4 "fileName": "0703WXb0839",
5 "filePath": "/我的文件/20240703/demo",
6 "extendName": "txt",
7 "isDir": 0
8 }
9
10 PUT http://127.0.0.1:9200/ws_index/_create/2/
11 {
12 "fileId": "83f311d71e5d4d799e5b254cf9305b04",
13 "fileName": "0703wxb0843",
14 "filePath": "/我的文件/20240703/demo",
15 "extendName": "txt",
16 "isDir": 0
17 }

2、查询(可以查到上面新增的两条数据)

 1 POST    http://127.0.0.1:9200/filesearch/_search
 2 {
3 "from": 0,
4 "size": 200,
5 "query": {
6 "bool": {
7 "must": [
8 {
9 "bool": {
10 "should": [
11 {
12 "wildcard": {
13 "fileName.keyword": "*Wxb*"
14 }
15 }
16 ]
17 }
18 },
19 {
20 "bool": {
21 "should": [
22 {
23 "wildcard": {
24 "filePath.keyword": "/我的文件/20240703/demo*"
25 }
26 }
27 ]
28 }
29 }
30 ]
31 }
32 }
33 }

@


前言

请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i


提示:以下是本篇文章正文内容,下面案例可供参考

背景

在项目出现上传文件,其中文件包含压缩包,并对压缩包的内容进行解析保存。

第一步:编写代码

1.1 请求层

我们用倒叙的方式来写。先写
ZipController

	@Autowired
    private ZipService zipService;

    /**
     * 上传二维码文件
     * @param qrCodeFile 二维码文件
     * @return 返回上传的结果
     */
    @ApiOperation(value = "上传二维码文件")
    @PostMapping("/uploadQrCodeFile")
    public Result uploadQrCodeFile(@RequestParam("file") MultipartFile qrCodeFile) throws Exception {
        zipService.uploadQrCodeFile(qrCodeFile);
        return Result.sendSuccess("上传成功");
    }

1.2 业务处理层

接着就是写
Service

@Service
public class ZipService {


    private static final Logger logger= LoggerFactory.getLogger(ZipService.class);


    public void uploadQrCodeFile(MultipartFile multipartFile)throws Exception {
        if (multipartFile.getSize() == 0
                || multipartFile.getOriginalFilename() == null
                || (multipartFile.getOriginalFilename() != null
                && !multipartFile.getOriginalFilename().contains("."))) {
            ExceptionCast.cast(Result.sendFailure("文件格式不正确或文件为空!"));
        }
        // 1.先下载文件到本地
        String originalFilename = multipartFile.getOriginalFilename();
        String destPath = System.getProperty("user.dir") + File.separator + "qrCodeFile";
        FileUtil.writeFromStream(
                multipartFile.getInputStream(), new File(destPath + File.separator + originalFilename));

        // 2.解压文件
        unzipAndSaveFileInfo(originalFilename, destPath);
        // 3.备份压缩文件,删除解压的目录
        FileUtils.copyFile(
                new File(destPath + File.separator + originalFilename),
                new File(destPath + File.separator + "backup" + File.separator + originalFilename));
        // 删除原来的上传的临时压缩包
        FileUtils.deleteQuietly(new File(destPath + File.separator + originalFilename));
        logger.info("文件上传成功,文件名为:{}", originalFilename);


    }

    /**
     * 解压和保存文件信息
     *
     * @param originalFilename 源文件名称
     * @param destPath         目标路径
     */
    private void unzipAndSaveFileInfo(String originalFilename, String destPath) throws IOException {
        if (StringUtils.isEmpty(originalFilename) || !originalFilename.contains(".")) {
            ExceptionCast.cast(Result.sendFailure("文件名错误!"));
        }
        // 压缩
        ZipUtil.unzip(
                new File(destPath + File.separator + originalFilename),
                new File(destPath),
                Charset.forName("GBK"));
        // 遍历文件信息
        String fileName = originalFilename.substring(0, originalFilename.lastIndexOf("."));
        File[] files = FileUtil.ls(destPath + File.separator + fileName);
        if (files.length == 0) {
            ExceptionCast.cast(Result.sendFailure("上传文件为空!"));
        }
        String targetPath = destPath + File.separator + "images";
        for (File file : files) {
            // 复制文件到指定目录
            String saveFileName =
                    System.currentTimeMillis() + new SecureRandom().nextInt(100) + file.getName();
            FileUtils.copyFile(file, new File(targetPath + File.separator + saveFileName));
            logger.info("文件名称:"+file.getName());
            logger.info("文件所在目录地址:"+saveFileName);
            logger.info("文件所在目录地址:"+targetPath + File.separator + saveFileName);
        }
    }
}

1.3 新增配置

因spring boot有默认上传文件大小限制,故需配置文件大小。在
application.properties
中添加
upload
的配置

#### upload begin  ###
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.max-file-size=10MB
#### upload end  ###

第二步:解压缩处理

2.1 引入依赖

引入
Apache
解压 / 压缩 工具类处理,解压
tar.gz
文件

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-compress -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.20</version>
</dependency>

2.2 解压缩工具类


  1. tar.gz
    转换为
    tar
  2. 解压
    tar
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


       // tar.gz 文件路径
        String sourcePath = "D:\\daleyzou.tar.gz";
        // 要解压到的目录
        String extractPath = "D:\\test\\daleyzou";
        File sourceFile = new File(sourcePath);
        // decompressing *.tar.gz files to tar
        TarArchiveInputStream fin = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(sourceFile)));
        File extraceFolder = new File(extractPath);
        TarArchiveEntry entry;
        // 将 tar 文件解压到 extractPath 目录下
        while ((entry = fin.getNextTarEntry()) != null) {
            if (entry.isDirectory()) {
                continue;
            }
            File curfile = new File(extraceFolder, entry.getName());
            File parent = curfile.getParentFile();
            if (!parent.exists()) {
                parent.mkdirs();
            }
            // 将文件写出到解压的目录
            IOUtils.copy(fin, new FileOutputStream(curfile));
        }

总结

我是
南国以南i
记录点滴每天成长一点点,学习是永无止境的!转载请附原文链接!!!

参考链接

参考链接

咕了2天才写的题解

还是比较经典的题目,分治处理网格图最短路

离线下来,利用分治的思想,用一条线把网格图平均劈成两半,每次只考虑询问在两块的一对点,所有的线必须经过直线上的一个点,于是我把线上所有点都在规定范围内跑一次dijkstra,最后直接算答案,显然我想让最短路跑的次数最小,每次选较短的边作为线段的长度,从较高的边上劈开,如果一组询问在同一块,分别递归,就是子问题了。这个做法正确性显然。

时间复杂度证明如下:

设点数有
\(|V|\)
个,当前分治在第
\(k\)
轮,那么线的长度就最多是
\(\sqrt{\frac{|V|}{2^k}}\)
,要进行线的长度次的dijkstra,因为网格图点数与边数同级,所以每次dijkstra时间复杂度是
\(O(\frac{|V|}{2^k}\log\frac{|V|}{2^k})\)
,即
\(O(\frac{|V|}{2^k}\log{|V|})\)
,然后第
\(k\)
轮,要分治
\(2^k\)
次,时间复杂度是
\(O(2^k\cdot\sqrt{\frac{|V|}{2^k}}\frac{|V|}{2^k}\log{|V|})\)
,整理得
\(\frac{|V|^{\frac{3}{2}}\cdot \log{|V|}}{2^{\frac{k}{2}}}\)
,有
\(k\)
轮,分母相加,由等比数列求和公式,知道是个常数,如果我没算错的话就是
\(\sqrt{2}+1\)
,然后时间复杂度就是
\(O(|V|^{\frac{3}{2}}\cdot \log{|V|})\)
,这道题
\(|V|\)
最大是2e4,这么算下来最多也只有4e7级别

#include<bits/stdc++.h>
#define vd void 
#define MAXN 200005 
#define pr std::pair<int,int>
#define fi first
#define se second 
const int inf=1e9;
int gi(){
	char c;int x=0,f=0;
	while(!isdigit(c=getchar()))f|=(c=='-');
	while(isdigit(c))x=(x*10)+(c^48),c=getchar();
	return f?-x:x;
}
template<class T>vd cnk(T&a,T b){a=a<=b?a:b;}
struct Q{
	int pos,st,ed;
}query[MAXN],g[MAXN],g2[MAXN];
int n,m,q,ans[MAXN],x[MAXN],y[MAXN],dis[MAXN];
bool vis[MAXN];
std::vector<pr>nbr[MAXN];
struct node{
	int to,W;
	bool friend operator<(node a,node b){return a.W>b.W;}
};
std::priority_queue<node>pq;
int id(int xx,int yy){return (xx-1)*m+yy;}
vd add(int xx,int yy,int w){nbr[xx].emplace_back(yy,w),nbr[yy].emplace_back(xx,w);}
vd dijkstra(int s,int xl,int xr,int yl,int yr){
	for(int i=xl;i<=xr;i++)for(int j=yl;j<=yr;j++)dis[id(i,j)]=inf,vis[id(i,j)]=0;
	dis[s]=0;pq.push({s,0});
	while(!pq.empty()){
		int u=pq.top().to;pq.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(pr v:nbr[u]){
			if(x[v.fi]<xl||x[v.fi]>xr||y[v.fi]<yl||y[v.fi]>yr)continue;
			if(dis[v.fi]>dis[u]+v.se)dis[v.fi]=dis[u]+v.se,pq.push({v.fi,dis[v.fi]});
		}
	}
}
vd solve(int ql,int qr,int xl,int xr,int yl,int yr){
	if(ql>qr||xl>xr||yl>yr)return;
	if(xr-xl>=yr-yl){
		int mid=(xl+xr)>>1,lg=0,lg2=0;
		for(int i=yl;i<=yr;i++){
			dijkstra(id(mid,i),xl,xr,yl,yr);
			for(int j=ql;j<=qr;j++)cnk(ans[query[j].pos],dis[query[j].st]+dis[query[j].ed]);
		}
		for(int i=ql;i<=qr;i++){
			if(x[query[i].st]<mid&&x[query[i].ed]<mid)g[++lg]=query[i];
			else if(x[query[i].st]>mid&&x[query[i].ed]>mid)g2[++lg2]=query[i];
		}
		for(int i=1;i<=lg;i++)query[ql+i-1]=g[i];
		for(int i=1;i<=lg2;i++)query[ql+lg+i-1]=g2[i];
		solve(ql,ql+lg-1,xl,mid-1,yl,yr),solve(ql+lg,ql+lg+lg2-1,mid+1,xr,yl,yr);
	}else{
		int mid=(yl+yr)>>1,lg=0,lg2=0;
		for(int i=xl;i<=xr;i++){
			dijkstra(id(i,mid),xl,xr,yl,yr);
			for(int j=ql;j<=qr;j++)cnk(ans[query[j].pos],dis[query[j].st]+dis[query[j].ed]);
		}
		for(int i=ql;i<=qr;i++){
			if(y[query[i].st]<mid&&y[query[i].ed]<mid)g[++lg]=query[i];
			else if(y[query[i].st]>mid&&y[query[i].ed]>mid)g2[++lg2]=query[i];
		}
		for(int i=1;i<=lg;i++)query[ql+i-1]=g[i];
		for(int i=1;i<=lg2;i++)query[ql+lg+i-1]=g2[i];
		solve(ql,ql+lg-1,xl,xr,yl,mid-1),solve(ql+lg,ql+lg+lg2-1,xl,xr,mid+1,yr);
	}
}
int main(){
	n=gi(),m=gi();
	for(int i=1;i<=n;i++)for(int j=1;j<m;j++){int w=gi();add(id(i,j),id(i,j+1),w);};
	for(int i=1;i<n;i++)for(int j=1;j<=m;j++){int w=gi();add(id(i,j),id(i+1,j),w);};
	q=gi();
	for(int i=1;i<=q;i++){
		int a=gi(),b=gi(),c=gi(),d=gi();
		query[i]={i,id(a,b),id(c,d)},ans[i]=inf;
	}
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)x[id(i,j)]=i,y[id(i,j)]=j;
	solve(1,q,1,n,1,m);
	for(int i=1;i<=q;i++)printf("%d\n",ans[i]);
	return 0;
}

算是一个套路吧,网格图最短路想分治