会话与 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+)
访问 Expires、MaxAge、SameSite 和 Partitioned 等完整字段:
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: true、HttpOnly: true和SameSite: 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{}和类型断言,丢失了编译时类型安全性。尽可能优先使用闭包或结构体模式。