Skip to content
Go
Gin
会话与 Cookie

会话与 Cookie

HTTP 是无状态的,会话和 Cookie 用于在多个请求之间维持用户状态。本章覆盖 Cookie 操作、基于 gin-contrib/sessions 的会话管理,以及依赖注入模式。


Cookie 操作

基本 API

// 设置 Cookie
c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)

// 读取 Cookie
value, err := c.Cookie("name")
参数 说明
name Cookie 名称
value Cookie 值
maxAge 生命周期(秒)。-1 = 删除,0 = 浏览器会话
path Cookie 有效的 URL 路径
domain Cookie 作用域
secure 仅 HTTPS 传输
httpOnly 阻止 JS 访问

基本示例

r.GET("/cookie", func(c *gin.Context) {
    cookie, err := c.Cookie("gin_cookie")

    if err != nil {
        cookie = "NotSet"
        // 首次访问:设置 Cookie
        c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
    }

    fmt.Printf("Cookie value: %s\n", cookie)
})

删除 Cookie

// maxAge = -1 即删除
c.SetCookie("gin_cookie", "test", -1, "/", "localhost", false, true)

使用 *http.Cookie 设置(v1.11+)

访问 ExpiresMaxAgeSameSitePartitioned 等完整字段:

r.GET("/set-cookie", func(c *gin.Context) {
    c.SetCookieData(&http.Cookie{
        Name:     "session_id",
        Value:    "abc123",
        Path:     "/",
        Domain:   "localhost",
        Expires:  time.Now().Add(24 * time.Hour),
        MaxAge:   86400,
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteLaxMode,
    })
    c.String(200, "ok")
})

🚨 陷阱:生产环境务必设置 Secure: trueHttpOnly: trueSameSite: Strict(或 Lax),以最大限度降低 CSRF 和 XSS 风险。


会话管理

gin-contrib/sessions 提供支持多种后端存储的会话管理:

go get github.com/gin-contrib/sessions

基于 Cookie 的会话

最简单的方式——会话数据存储在加密 Cookie 中:

import (
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    store := cookie.NewStore([]byte("your-secret-key"))
    r.Use(sessions.Sessions("mysession", store))

    r.GET("/login", func(c *gin.Context) {
        session := sessions.Default(c)
        session.Set("user", "john")
        session.Save()
        c.JSON(200, gin.H{"message": "logged in"})
    })

    r.GET("/profile", func(c *gin.Context) {
        session := sessions.Default(c)
        user := session.Get("user")
        if user == nil {
            c.JSON(401, gin.H{"error": "not logged in"})
            return
        }
        c.JSON(200, gin.H{"user": user})
    })

    r.GET("/logout", func(c *gin.Context) {
        session := sessions.Default(c)
        session.Clear()
        session.Save()
        c.JSON(200, gin.H{"message": "logged out"})
    })

    r.Run(":8080")
}

基于 Redis 的会话

生产环境推荐——跨多实例可扩展:

import (
    "github.com/gin-contrib/sessions/redis"
)

store, err := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
if err != nil {
    panic(err)
}
r.Use(sessions.Sessions("mysession", store))

会话选项

session := sessions.Default(c)
session.Options(sessions.Options{
    Path:     "/",
    MaxAge:   3600,               // 1 小时后过期(秒)
    HttpOnly: true,               // 防止 JS 访问
    Secure:   true,               // 仅 HTTPS 发送
    SameSite: http.SameSiteLaxMode,
})
选项 说明
Path Cookie 路径范围(默认 /
MaxAge 生命周期(秒)。-1 删除,0 浏览器会话
HttpOnly 防止 JavaScript 访问 Cookie
Secure 仅通过 HTTPS 发送 Cookie
SameSite 跨站 Cookie 行为(Lax / Strict / None

可用后端

后端 适用场景
Cookie sessions/cookie 简单应用,小型会话数据
Redis sessions/redis 生产环境,多实例部署
Memcached sessions/memcached 高性能缓存层
MongoDB sessions/mongo 以 MongoDB 为主要存储
PostgreSQL sessions/postgres 以 PostgreSQL 为主要存储

会话 vs JWT

方面 会话 JWT
存储 服务端(Redis、数据库) 客户端(令牌本身)
撤销 简单(从存储中删除) 困难(需要黑名单机制)
可扩展性 需要共享存储 无状态,天然可扩展
数据大小 服务端无限制 受令牌大小限制
调试 简单(查看存储) 需解码令牌

💡 选择指南

  • 需要即时撤销(如封禁用户、强制登出)→ 会话
  • 需要跨微服务无状态认证 → JWT
  • 混合方案:JWT 做认证 + 少量状态存 Redis

依赖注入模式

随着应用增长,需要在处理函数之间共享数据库连接、配置和服务等依赖。

模式对比

模式 类型安全 可测试性 适用场景
闭包 编译时 容易 mock 小型应用,少量依赖
结构体 编译时 容易 mock 中大型应用
中间件 运行时 中等 横切关注点,共享状态

闭包模式

最符合 Go 习惯——通过闭包传递依赖:

func PingHandler(db *sql.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        if err := db.Ping(); err != nil {
            c.JSON(503, gin.H{"error": "database unreachable"})
            return
        }
        c.JSON(200, gin.H{"status": "ok"})
    }
}

func main() {
    db, _ := sql.Open("postgres", "...")
    r := gin.Default()
    r.GET("/ping", PingHandler(db))
    r.Run(":8080")
}

基于结构体的处理函数

中大型应用推荐:

type App struct {
    DB     *sql.DB
    Logger *slog.Logger
    Config *Config
}

func (a *App) GetUser(c *gin.Context) {
    id := c.Param("id")
    var name string
    err := a.DB.QueryRowContext(c.Request.Context(),
        "SELECT name FROM users WHERE id = $1", id).Scan(&name)
    if err != nil {
        a.Logger.Error("user not found", "id", id)
        c.JSON(404, gin.H{"error": "user not found"})
        return
    }
    c.JSON(200, gin.H{"id": id, "name": name})
}

func main() {
    app := &App{
        DB:     db,
        Logger: slog.Default(),
    }
    r := gin.Default()
    r.GET("/users/:id", app.GetUser)
    r.POST("/users", app.CreateUser)
    r.Run(":8080")
}

中间件注入模式

通过中间件将依赖注入到请求上下文:

func DatabaseMiddleware(db *sql.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("db", db)
        c.Next()
    }
}

func GetUser(c *gin.Context) {
    db := c.MustGet("db").(*sql.DB)
    // 使用 db...
}

🚨 陷阱:中间件模式使用 interface{} 和类型断言,丢失了编译时类型安全性。尽可能优先使用闭包或结构体模式。