第三章 Web框架(Gin) v1.0
第一部分 快速搭建
一、项目初始化
# 初始化Go模块
go mod init app
# 安装Gin框架
go get -u github.com/gin-gonic/gin
二、基本结构
下面示例创建了一个简单的 Gin 应用程序
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// 创建基础引擎
r := gin.New()
helloGroup := r.Group("/hello")
{
helloGroup.GET("/", func(c *Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "hello",
})
})
}
// 启动服务器,默认监听8080端口
r.Run(":8080")
}
第二部分 引擎与路由
一、Gin引擎
Engine 是 Gin 应用的实例,相当于整个 Web 应用的 "大脑",负责管理路由、中间件、配置选项等核心功能
// 创建一个默认配置的、全新的、无默认中间件的引擎实例
engine := gin.New()
// 创建默认配置的、全新的、带默认日志(Logger)和错误恢复(Recovery)中间件的引擎实例
// 本质上是先调用New()创建基础引擎,然后通过Use()方法添加中间件
engine := gin.Default()
// 从 New() 方法源码可以具体查看其默认配置,也可以根据项目情况进行高度定制
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
engine := &Engine{
// 创建了一个空的路由组作为根路由
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{}, // 初始化模板函数映射(用于HTML渲染)
RedirectTrailingSlash: true, // 自动重定向带斜杠的路由到不带斜杠的版本(如/foo/重定向到/foo)
RedirectFixedPath: false, // 禁用错误路径自动修复
HandleMethodNotAllowed: false, // 不处理HTTP方法不允许的情况(默认返回404)
ForwardedByClientIP: true, // 允许通过请求头获取客户端真实IP
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, // 用于获取客户端IP的请求头列表
TrustedPlatform: defaultPlatform, // 设置默认的可信平台(用于获取客户端IP)
UseRawPath: false, // 不使用原始 URL 路径,会对 URL 进行转义处理
RemoveExtraSlash: false, // 不移除URL中的额外斜杠(如//foo///bar不会转为/foo/bar)
UnescapePathValues: true, // 自动对路径参数进行 URL 解码(如将%20转为空格)
MaxMultipartMemory: defaultMultipartMemory, // 设置处理文件上传的默认内存限制(32MB)
trees: make(methodTrees, 0, 9), // 初始化路由树(支持9种HTTP方法)
delims: render.Delims{Left: "{{", Right: "}}"}, // 设置默认的 HTML 模板分隔符( "{{" "}}" )
secureJSONPrefix: "while(1);", // 为JSON响应添加安全前缀(防止JSON劫持)
trustedProxies: []string{"0.0.0.0/0", "::/0"}, // 默认信任所有代理IP(适用于开发环境)
trustedCIDRs: defaultTrustedCIDRs, // 默认的可信CIDR范围(用于IP验证)
}
engine.RouterGroup.engine = engine
// 初始化上下文对象池(sync.Pool),初始容量由maxParams决定
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine.With(opts...)
}
二、路由注册
使用 Gin 定义路由方式简洁,通过 HTTP 方法来注册处理函数
// 所有HTTP方法路由(如 GET/POST)的底层实现
func (group *RouterGroup) Handle(httpMethod string, relativePath string, handlers ...HandlerFunc) IRoutes
// 一次性为指定路径注册所有标准HTTP方法的路由
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes
// 为指定路径注册指定HTTP方法的路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes
// 将单个本地文件映射到指定的路由路径,客户端访问该路径时直接返回对应的文件
func (group *RouterGroup) StaticFile(relativePath string, filepath string) IRoutes
// 通过自定义 http.FileSystem 读取文件,其他作用同 StaticFile 方法
func (group *RouterGroup) StaticFileFS(relativePath string, filepath string, fs http.FileSystem) IRoutes
// 将整个本地目录映射到指定路由路径,客户端可通过该路径访问目录下的所有文件(支持子目录递归访问)
func (group *RouterGroup) Static(relativePath string, root string) IRoutes
// 通过自定义 http.FileSystem 读取文件,其他作用同 Static 方法
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user/:id", func(c *Context) {
// 获取路径参数, :id 为路径参数占位符
id := c.Param("id")
c.JSON(200, gin.H{"user_id": id})
})
r.Run()
}
二、分组路由
Gin 支持路由分组,相关路由可拥有共同前缀并共享中间件
// 创建一个新的路由组,为该路由组添加共同 relativePath 前缀,并共享中间件
Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
package main
import "github.com/gin-gonic/gin"
func main() {
// 根路由组
r := gin.Default()
// 创建用户模块路由组,路径前缀:/user
userGroup := r.Group("/user")
// 组内路由,实际路径:/user/login
userGroup.POST("/login", func(c *Context) {
c.JSON(200, gin.H{"msg": "user login"})
})
// 组内路由,实际路径:/user/profile
userGroup.GET("/profile", func(c *Context) {
c.JSON(200, gin.H{"msg": "user profile"})
})
r.Run()
}
第二部分 上下文与中间件
Gin 中的上下文由
*gin.Context
类型表示,是请求处理的核心载体,贯穿整个请求生命周期,负责在请求、中间件和处理函数之间传递数据、管理流程
一、核心作用
- 数据传递:
- 通过 c.Set(key, value) 在中间件或处理函数中存储数据,通过 c.Get(key) 或 c.MustGet(key) 在后续流程中获取,实现不同处理阶段的数据共享(如用户身份信息、请求 ID 等)。
- 流程控制:
- c.Next():暂停当前中间件,执行后续中间件或处理函数,完成后返回继续执行当前中间件剩余逻辑(用于 “前置处理 - 后置处理” 场景)。
- c.Abort():中断请求流程,不再执行后续中间件或处理函数(常用于权限校验失败等场景)。
- c.AbortWithStatus(code):中断流程并返回指定 HTTP 状态码。
- 请求与响应管理
- 封装了 HTTP 请求(c.Request)和响应(c.Writer),提供便捷方法操作请求参数(如 c.Param()、c.Query())、请求头(c.GetHeader())、Cookie(c.Cookie())等
- 提供响应方法(如 c.JSON()、c.String()、c.HTML()),快速返回不同格式的响应
二、中间件
中间件实际是
gin.HandlerFunc
类型的函数,用于在请求到达处理函数之前或之后执行通用逻辑
1、定义与使用
// 直接定义的中间件函数
func MiddlewareName(c *Context) {
// 前置处理逻辑(请求到达处理函数前执行)
c.Next() // 调用后续的中间件或处理函数
// 后置处理逻辑(处理函数执行完成后执行)
}
// 工厂函数(推荐使用,支持更复杂场景)
func MiddlewareFuncName() gin.HandlerFunc {
return func(c *Context) {
// 前置处理逻辑(请求到达处理函数前执行)
c.Next() // 调用后续的中间件或处理函数
// 后置处理逻辑(处理函数执行完成后执行)
}
}
r := gin.New()
// 全局注册LoggerMiddleware中间件
r.Use(LoggerMiddleware())
// 创建路由组并应用中间件
apiGroup := r.Group("/api", FirstMiddleware(), SecondMiddleware())
// 通过Use方法为已创建的路由组添加中间件
apiGroup.Use(OtherMiddleware())
// 为单路由注册AuthMiddleware中间件
r.GET("/profile", AuthMiddleware(), func(c *Context) {
c.JSON(200, gin.H{"msg": "message"})
})
2、流程控制
/// 后续处理
// 执行后续的中间件和处理器,之后返回继续执行当前中间件
func (c *Context) Next()
/// 中断处理
// 中断请求处理,不再执行后续中间件和处理器
func (c *Context) Abort()
// 中断并返回指定状态码
func (c *Context) AbortWithStatus(code int)
// 中断并返回指定状态码和JSON数据
func (c *Context) AbortWithStatusJSON(code int, jsonObj any)
// 中断请求流程并记录错误
func (c *Context) AbortWithError(code int, err error) *Error
三、数据传递
// 向当前上下文中存储键值对数据,数据仅在当前请求的生命周期内有效
func (c *Context) Set(key string, obj any)
// 从上下文中获取指定键对应的值,并返回一个布尔值表示该键是否存在
func (c *Context) Get(key string) (any, bool)
// 从上下文中获取指定键对应的值,若键不存在则直接 panic
func (c *Context) MustGet(key string) any
// 存储数据到上下文
c.Set("user_id", 123)
c.Set("username", "admin")
// 获取数据
if userId, exists := c.Get("user_id"); exists {
// 类型断言
id := userId.(int)
}
// 确保能获取到数据并进行类型断言
username := c.MustGet("username").(string)
第三部分 请求与响应
一、请求参数获取
直接从 URL/查询参数等 获取单个参数,返回字符串类型,适合简单参数场景
1、查询参数
// 获取参数,不存在则返回空字符串
func (c *Context) Query(key string) string
// 获取参数,不存在则返回默认值
func (c *Context) DefaultQuery(key string, defaultValue string) string
// 获取参数,同时返回布尔值表示是否存在
func (c *Context) GetQuery(key string) (string, bool)
2、路径参数
// 获取动态路径参数,不存在则返回空字符串
func (c *Context) Param(key string) string
3、获取请求头
// 获取指定的请求头字段值(键名不区分大小写),不存在则返回空字符串
func (c *Context) GetHeader(key string) string
// 获取Cookie信息,如果Cookie不存在或读取失败则返回错误
func (c *Context) Cookie(name string) (string, error)
二、请求参数绑定
将请求数据(JSON、表单、路径参数等)自动映射到 Go 结构体,支持数据验证,适合处理复杂参数
1、映射结构体
①结构体定义
type ExampleEnt struct {
Feild string `参数标签:"feild" binding:"结构体标签"`
}
type UserCreateRequestEnt struct {
Id string `uri:"id" binding:"required,min=2"` // 绑定路径参数,必填,最少 2 个字符
Name string `json:"name" binding:"required,min=2"` // 绑定json参数,必填,最少 2 个字符
Email string `json:"email" binding:"required,email"` // 绑定json参数,必填,邮箱格式
Age int `json:"age" binding:"min=0,max=150"` // 绑定json参数,数值范围
}
`json:"feild"` // 对应 json 绑定,字段名与JSON中的键对应
`form:"feild"` // 对应 查询参数 绑定,字段名与查询参数的键对应
`header:"feild"` // 对应 请求头 绑定,字段名与请求头的键对应
`uri:"feild"` // 对应 路径参数 绑定,字段名与路径参数的占位符对应
`xml:"feild"` // 对应 xml格式数据 绑定
`yaml:"feild"` // 对应 yaml格式数据 绑定
`toml:"feild"` // 对应 toml格式数据 绑定
②参数校验
结构体标签(基于 validator 库)实现参数校验,常用标签:
required
:必填项omitempty
:字段为空忽略验证min/max
:数值或字符串长度范围email
:邮箱格式ip/ipv4/ipv6
:必须是合法IP地址(v4或v6/v4/v6)regexp=^pattern$
:正则匹配
2、Bind与ShouldBind
/// Bind 系列方法 绑定参数到结构体,若解析失败,返回 400 Bad Request 并终止请求,简化错误处理逻辑
// 根据请求的 方法 和 Content-Type 自动选择绑定器,例如:POST/PUT 请求且 Content-Type="application/json" → 使用 JSON 解析;Content-Type="application/xml" → 使用 XML 解析
func (c *Context) Bind(obj any) error
// 强制使用JSON绑定,不检查Content-Type
func (c *Context) BindJSON(obj any) error
// 绑定查询参数到结构体
func (c *Context) BindQuery(obj any) error
// 绑定请求头参数到结构体
func (c *Context) BindHeader(obj any) error
// 绑定路径参数到结构体
func (c *Context) BindUri(obj any) error
// 强制使用XML绑定,不检查Content-Type
func (c *Context) BindXML(obj any) error
// 以YAML格式解析请求体,将数据绑定到结构体
func (c *Context) BindYAML(obj any) error
// 以TOML格式解析请求体,将数据绑定到结构体
func (c *Context) BindTOML(obj any) error
// 所有Bind方法的底层实现,使用指定的绑定器(如 binding.JSON)进行数据绑定,若解析失败,返回400错误并终止请求
func (c *Context) MustBindWith(obj any, b binding.Binding) error
// ShouldBind 系列方法 绑定参数到结构体,若解析失败,仅返回错误,不会自动向客户端响应
// 根据请求的 方法 和 Content-Type 自动选择绑定器,例如:POST/PUT 请求且 Content-Type="application/json" → 使用 JSON 解析;Content-Type="application/xml" → 使用 XML 解析
func (c *Context) ShouldBind(obj any) error
// 强制使用JSON绑定,不检查Content-Type
func (c *Context) ShouldBindJSON(obj any) error
// 绑定查询参数到结构体
func (c *Context) ShouldBindQuery(obj any) error
// 绑定请求头参数到结构体
func (c *Context) ShouldBindHeader(obj any) error
// 绑定路径参数到结构体
func (c *Context) ShouldBindUri(obj any) error
// 强制使用XML绑定,不检查Content-Type
func (c *Context) ShouldBindXML(obj any) error
// 以YAML格式解析请求体,将数据绑定到结构体
func (c *Context) ShouldBindYAML(obj any) error
// 以TOML格式解析请求体,将数据绑定到结构体
func (c *Context) ShouldBindTOML(obj any) error
// 所有ShouldBind方法的底层实现,使用指定的绑定器(如 binding.JSON)进行数据绑定,若解析失败,仅返回错误
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error
三、请求响应
1、基础响应
// 设置响应状态码并返回格式化字符串响应
func (c *Context) String(code int, format string, values ...interface{})
// 设置响应状态码并返回 JSON 响应(自动设置 Content-Type: application/json)
func (c *Context) JSON(code int, obj interface{})
// 设置响应状态码并返回 XML 响应(自动设置 Content-Type: application/xml)
func (c *Context) XML(code int, obj interface{})
// 设置响应状态码并渲染 HTML 模板并返回(需预先加载模板)
func (c *Context) HTML(code int, name string, obj interface{})
// 所有响应方法的底层实现
func (c *Context) Render(code int, r render.Render)
// 单独设置响应状态码,Render方法底层调用了该方法设置响应状态码
func (c *Context) Status(code int)
对于HTML响应,渲染HTML模板需要预先在Gin引擎中加载模板
// 加载符合pattern的多个模板,支持通配符*,如"templates/**/*"
func (engine *Engine) LoadHTMLGlob(pattern string)
// 精确指定需要加载的模板文件路径
func (engine *Engine) LoadHTMLFiles(files ...string)
/// 模板语法
// Gin框架默认使用的是Go语言标准库的模板语法(html/template包),在此不过多赘述
/// 常用模板语法 - 变量渲染
// {{.变量名(.变量属性)}
Gin模板的唯一标识是 define 定义的名称,若为定义则默认使用文件名;同时,加载同一唯一标识的模板会被后加载的模板覆盖
{{define "template1"}}
<h1>{{.template_name}}</h1>
{{end}}
r.LoadHTMLFiles("templates/index.html")
r.GET("/template", func(c *gin.Context) {
c.HTML(http.StatusOK, "template1", gin.H{
"template_name": "template1",
})
})
2、设置响应头
// 设置响应头字段
func (c *Context) Header(key, value string)
// 向客户端设置 Cookie
func (c *Context) SetCookie(
name string, // Cookie键名
value string, // Cookie值
maxAge int, // 有效期: 正数表示在maxAge秒后过期,客户端会自动删除; 负数(通常为-1)表示有效期为当前会话,关闭浏览器后自动失效; 0表示立即删除该Cookie
path string, // Cookie的生效路径,"/" 表示全站所有路径都生效,"/path" 表示仅在访问 /path 开头路径时携带,""表示当前请求的路径
domain string, // 生效域名,限制Cookie仅在指定域名下被携带,""表示仅在当前域名生效,"domain.com"表示在domain.com及其子域名生效
secure bool, // 是否仅通过HTTPS协议传输,防止HTTP明文传输被窃听
httpOnly bool // 是否禁止JavaScript访问此Cookie,有效防止XSS(跨站脚本攻击)窃取Cookie
)
3、重定向
// 重定向到指定 URL(状态码通常为 301(StatusMovedPermanently) 或 302 (Found))
func (c *Context) Redirect(code int, location string)
4、文件响应
// 发送指定路径的文件(自动根据文件扩展名推断 MIME 类型)
func (c *Context) File(path string)
// 发送文件并指定下载时的文件名(触发浏览器下载)
func (c *Context) FileAttachment(path, filename string)
// 通过io.Reader流返回文件数据
func (c *Context) DataFromReader(
code int, // HTTP 响应状态码
contentLength int64, // 文件大小 (字节数,-1表示未知)
contentType string, // MIME 类型
reader io.Reader, // 文件数据的读取流 (如: os.File/bytes.Reader)
extraHeaders map[string]string // 额外的响应头 (如: Content-Disposition)
)
第四部分 进阶应用
一、运行与优雅退出
1、启动运行
在 Gin 框架中,r.Run() 是启动 HTTP 服务的常用方法,其本质是一个阻塞式调用(内部会启动 http.Server 并阻塞当前 goroutine)
优点:使用简单,r.Run() 封装了创建 http.Server、绑定地址、启动监听 等底层逻辑,适合快速开发
缺点:灵活性差,难以定制服务参数;无法实现优雅退出
if err := r.Run(); err != nil {
log.Fatalf("run fail: %v", err)
}
func (engine *Engine) Run(addr ...string) (err error) {
// 延迟执行错误打印函数:无论 Run 方法因何种原因退出,都会打印错误信息
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
// 解析监听地址:若未传入 addr,默认使用 ":8080"; 若传入多个 addr,取第一个有效地址
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
// 调用标准库 http.ListenAndServe 启动服务
err = http.ListenAndServe(address, engine.Handler())
return
}
2、实现优雅退出
通过 http.Server 实例的 ListenAndServe() 启动服务,从而可以调用 Shutdown() 方法实现优雅关闭。
func main() {
// 创建可被系统信号取消的根上下文
globalCtx, globalCancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // kill 命令
syscall.SIGQUIT, // Ctrl+\
)
defer globalCancel() // 程序退出前触发全局清理
// 初始化Gin引擎及路由
r := gin.Default()
// 显式创建http.Server
server := &http.Server{
Addr: ":8080", // 监听地址
Handler: r, // 绑定Gin引擎
}
// 启动服务器,默认监听8080端口
// 启动Gin服务(非阻塞)
go func() {
// 启动服务
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
os.Exit(1) // 启动失败时退出程序
}
}()
// 等待全局退出信号(如Ctrl+C、kill)
<-globalCtx.Done()
// 关闭Gin服务(等待正在处理的请求完成,超时5秒)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 调用Shutdown,通知服务停止接受新请求并等待现有请求完成
if err := server.Shutdown(shutdownCtx); err != nil {
os.Exit(1)
}
}
二、中间件应用示例
编写一个密码认证中间件,模拟实现一个登录后需要二次认证的功能
应用场景:重要资源需要二次输入密码验证后方可访问
# 定义环境
@dev_baseUrl = http://localhost:8080
# 默认使用开发环境
@baseUrl = {{dev_baseUrl}}
### 登录接口
POST {{baseUrl}}/user/login
Content-Type: application/json
{
"username": "admin",
"password": "admin"
}
### 校验密码接口
POST {{baseUrl}}/user/verify
Content-Type: application/json
{
"username": "admin",
"password": "admin"
}
### 访问限制资源接口
GET {{baseUrl}}/user/hello
Accept: application/json
# 直接调用/登录后直接调用 -> (500) {"msg": "get verify app fail"} [访问被拒绝]
# 登录后验证密码后调用 -> (200) {"msg": "hello"} [正常访问资源]
package main
import (
"context"
"errors"
"net/http"
"os"
"time"
"example/app"
"example/middleware/verify"
"github.com/gin-gonic/gin"
)
type LoginEeqEnt struct {
Username string `json:"username" `
Password string `json:"password"`
}
type VerifyPasswordReqEnt struct {
Password string `json:"password"`
}
func Login(c *gin.Context) {
req := LoginEeqEnt{}
// 获取请求参数
if err := c.ShouldBindJSON(&req); err != nil {
verify.GetMiddleware().VerifyFailFunc(c, err)
return
}
// 密码校验逻辑
if req.Username == "admin" && req.Password == "admin" {
// 校验通过
/// 此处模拟向jwt的payload中写入username
c.SetCookie("jwt-payload", req.Username, 0, "/", "", true, true)
c.JSON(200, gin.H{"msg": "OK"})
return
}
verify.GetMiddleware().VerifyFailFunc(c, errors.New("verify password err"))
}
func VerifyPassword(c *gin.Context) {
req := VerifyPasswordReqEnt{}
// 获取请求参数
if err := c.ShouldBindJSON(&req); err != nil {
verify.GetMiddleware().VerifyFailFunc(c, err)
return
}
/// 此处模拟从jwt的payload中取出username
if username, err := c.Cookie("jwt-payload"); err != nil {
verify.GetMiddleware().VerifyFailFunc(c, err)
return
} else if username == "admin" && req.Password == "admin" {
// 校验通过
verify.GetMiddleware().GenerateContext(c)
c.JSON(200, gin.H{"msg": "OK"})
return
}
verify.GetMiddleware().VerifyFailFunc(c, errors.New("verify password err"))
}
func Hello(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "hello"})
}
func main() {
app.InitGlobalContext()
defer app.Shutdown() // 程序退出前触发全局清理
// 初始化Gin引擎及路由
r := gin.Default()
userGroup := r.Group("/user")
{
// 登录
userGroup.POST("/login", Login)
// 校验密码
userGroup.POST("/verify", VerifyPassword)
// 限制资源,必须校验密码后才能访问
userGroup.GET("/hello", verify.GetMiddleware().CheckContext, Hello)
}
// 显式创建http.Server
server := &http.Server{
Addr: ":8080", // 监听地址
Handler: r, // 绑定Gin引擎
}
// 启动服务器,默认监听8080端口
// 启动Gin服务(非阻塞)
go func() {
// 启动服务
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
os.Exit(1) // 启动失败时退出程序
}
}()
// 等待全局退出信号(如Ctrl+C、kill)
<-app.GlobalContext().Done()
// 关闭Gin服务(等待正在处理的请求完成,超时5秒)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 调用Shutdown,通知服务停止接受新请求并等待现有请求完成
if err := server.Shutdown(shutdownCtx); err != nil {
os.Exit(1)
}
}
package app
import (
"context"
"os/signal"
"sync"
"syscall"
)
var (
globalCtx context.Context
globalCancel context.CancelFunc
once sync.Once // 确保初始化只执行一次
)
// InitGlobalContext 初始化全局上下文,应在程序启动时调用一次
func InitGlobalContext() {
once.Do(func() {
// 创建可被系统信号取消的根上下文
globalCtx, globalCancel = signal.NotifyContext(
context.Background(),
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // kill 命令
syscall.SIGQUIT, // Ctrl+\
)
})
}
// GlobalContext 返回全局根上下文,其他包可通过此获取退出信号
func GlobalContext() context.Context {
if globalCtx == nil {
// 未初始化根上下文
panic("global context not initialized")
}
return globalCtx
}
// Shutdown 触发全局上下文取消,通知所有依赖退出
func Shutdown() {
if globalCancel != nil {
globalCancel() // 触发所有依赖此上下文的组件退出
}
}
package verify
import (
"container/list"
"example/app"
"sync"
"time"
uuid "github.com/satori/go.uuid"
)
// TokenManagerImpl 令牌管理结构体
type TokenManagerImpl struct {
tokens map[string]*list.List
expireTime time.Duration
mutex sync.RWMutex
once sync.Once
}
// Token 验证token结构体
type Token struct {
Content string
ExpireTime time.Time
}
// AddToken 添加Token
func (tm *TokenManagerImpl) AddToken(key string) string {
tm.mutex.Lock()
defer tm.mutex.Unlock()
if _, exists := tm.tokens[key]; !exists {
tm.tokens[key] = list.New()
}
// 使用uuid生成随机uuid作为令牌
tokenContent := uuid.NewV4().String()
expireTime := time.Now().Add(tm.GetExpireTime())
tm.tokens[key].PushBack(Token{
Content: tokenContent,
ExpireTime: expireTime,
})
return tokenContent
}
// CheckAndRemoveToken 检查Token有效性并清除
func (tm *TokenManagerImpl) CheckAndRemoveToken(key string, content string) bool {
tm.mutex.Lock()
defer tm.mutex.Unlock()
// 初始化定时清理线程
tm.once.Do(func() {
ticker := time.NewTicker(1 * time.Minute)
go func() {
defer ticker.Stop() // 退出时停止ticker,释放资源
for {
select {
case <-ticker.C:
// 执行清理逻辑
tm.clearExpiredTokens()
case <-app.GlobalContext().Done():
// 根上下文结束,退出goroutine
return
}
}
}()
})
tokenList, exists := tm.tokens[key]
if !exists || tokenList.Len() == 0 {
return false
}
for e := tokenList.Front(); e != nil; e = e.Next() {
token := e.Value.(Token)
if token.Content == content {
tokenList.Remove(e)
return true
}
}
return false
}
// GetExpireTime 获取过期时间配置
func (tm *TokenManagerImpl) GetExpireTime() time.Duration {
return tm.expireTime
}
// clearExpiredTokens 清除过期Tokens
func (tm *TokenManagerImpl) clearExpiredTokens() {
now := time.Now()
for key := range tm.tokens {
tokenList := tm.tokens[key]
for e := tokenList.Front(); e != nil; {
next := e.Next()
token := e.Value.(Token)
if token.ExpireTime.Before(now) {
tokenList.Remove(e)
}
e = next
}
// 如果用户列表为空,删除列表
if tokenList.Len() == 0 {
delete(tm.tokens, key)
}
}
}
package verify
import (
"errors"
"net/http"
"sync"
"time"
"container/list"
"github.com/gin-gonic/gin"
)
var (
m *Middleware
once sync.Once
)
func GetMiddleware() *Middleware {
once.Do(func() {
var err error
m, err = New(&Middleware{
GetKeyFunc: func(c *gin.Context) (string, error) {
v, err := c.Cookie("jwt-payload")
if err != nil {
return "", errors.New("payload not exist")
}
return v, nil
},
})
if err != nil {
panic(err)
}
})
return m
}
type TokenManager interface {
// AddToken 指定key生成并返回令牌
AddToken(key string) string
// CheckAndRemoveToken 检查对应key的令牌是否有效,并移除过期的令牌
CheckAndRemoveToken(key string, content string) bool
// GetExpireTime 获取过期时间
GetExpireTime() time.Duration
}
type Middleware struct {
GetKeyFunc func(c *gin.Context) (string, error)
VerifyFailFunc func(c *gin.Context, err error)
VerifyCookieKey string
TokenManager TokenManager
ExpireTime time.Duration
}
var (
ErrUnimplementedGetKeyFunc = errors.New("unimplemented method: GetKeyFunc")
ErrGetVerifyKey = errors.New("get verify key fail")
ErrGetVerifyContext = errors.New("get verify app fail")
ErrCheckContext = errors.New("check fail")
)
// New for check error with Middleware
func New(m *Middleware) (*Middleware, error) {
if err := m.MiddlewareInit(); err != nil {
return nil, err
}
return m, nil
}
func (vm *Middleware) MiddlewareInit() error {
if vm.GetKeyFunc == nil {
return ErrUnimplementedGetKeyFunc
}
if vm.ExpireTime == 0 {
vm.ExpireTime = 5 * time.Minute
}
if vm.TokenManager == nil {
vm.TokenManager = &TokenManagerImpl{
tokens: make(map[string]*list.List),
expireTime: vm.ExpireTime,
}
}
if vm.VerifyFailFunc == nil {
vm.VerifyFailFunc = func(c *gin.Context, err error) {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": err.Error(),
})
c.Abort()
}
}
if vm.VerifyCookieKey == "" {
vm.VerifyCookieKey = "verify-token"
}
return nil
}
func (vm *Middleware) CheckContext(c *gin.Context) {
// 获取用户身份信息
key, err := vm.GetKeyFunc(c)
if err != nil {
vm.VerifyFailFunc(c, ErrGetVerifyKey)
return
}
// 获取verify参数
verify, err := c.Cookie(vm.VerifyCookieKey)
if err != nil {
vm.VerifyFailFunc(c, ErrGetVerifyContext)
return
}
// 清除Cookie
c.SetCookie(vm.VerifyCookieKey, "", -1, "/", "", true, true)
// 验证
if vm.TokenManager.CheckAndRemoveToken(key, verify) {
// 检查通过
c.Next()
} else {
vm.VerifyFailFunc(c, ErrCheckContext)
return
}
}
func (vm *Middleware) GenerateContext(c *gin.Context) {
// 获取用户身份信息
key, err := vm.GetKeyFunc(c)
if err != nil {
vm.VerifyFailFunc(c, ErrGetVerifyKey)
return
}
// 生成令牌
verifyToken := vm.TokenManager.AddToken(key)
c.SetCookie(vm.VerifyCookieKey, verifyToken, 0, "/", "", true, true)
}