必须用c.SetCookie()和c.Cookie()操作Cookie、gin-contrib/sessions管理Session;Cookie存轻量标识,Session存敏感数据并落Redis;手动拼Header或自实现Session易致XSS、panic、多实例不共享。

直接用 c.SetCookie() 和 c.Cookie() 读写 Cookie,别自己拼 Set-Cookie 头;Session 必须用 gin-contrib/sessions,自己用 map + sync.Mutex 模拟会崩、丢数据、不共享。
Cookie 读写必须走 Gin 封装函数
手动调用 http.SetCookie() 或直接写 Header().Set("Set-Cookie", ...) 极易出错:漏掉 HttpOnly=true 导致 XSS,传错 maxAge 类型(必须是 int,不是 string 或 time.Duration),或忘记 URL 编码含空格/中文的值。
c.SetCookie() 内部已做三件事:自动 url.QueryEscape() 值、标准化过期时间、安全设置 Secure/HttpOnly 标志。读取时 c.Cookie("name") 返回的是解码后原始值,不用再 url.QueryUnescape()。
-
maxAge = -1:浏览器收到后立即删除该 Cookie -
maxAge = 0:退化为会话级 Cookie(关浏览器即失效) -
domain开发填"localhost",上线必须改成不含端口的域名,如"example.com","example.com:8080"无效 -
secure = true时,HTTP 请求无法携带该 Cookie —— 本地调试务必设为false
Session 必须用 gin-contrib/sessions,不能手写
Gin 本身不提供 Session 支持。gin.Default() 里没有 Session 中间件。自己用全局 map 存 Session,会遇到三个硬伤:
- 并发读写 panic(没加锁)或数据竞争(加锁但逻辑错)
- 服务重启后全部丢失
- 多实例部署时 Session 不共享,用户在 A 实例登录,请求转到 B 实例就变未登录
标准做法是引入 github.com/gin-contrib/sessions,它抽象了存储后端。最常用的是 Redis 方案:redis.NewStore(...) 接收的是已初始化的 *redis.Client,不是字符串地址。
Session ID 默认存在名为 "session" 的 Cookie 中,且默认启用 HttpOnly=true 和 Secure=true(生产环境)。改名需同步调整前后端,否则读不到。
Session 读写要显式 Save,且注意类型断言
session.Get("user_id") 返回 interface{},必须做类型断言才能用:
if v, ok := session.Get("user_id").(int64); ok {
// 使用 v
}修改 Session 后必须调用 session.Save(),否则变更不会写入后端 —— 这点被跳过会导致“存了但下次读不到”,调试时容易误判为中间件没生效。
- Session 数据建议只存轻量标识(如
user_id),敏感信息(如密码、token)不要放进去 - Cookie 存 Session ID,Session 本体存 Redis,二者协同才安全
- 别试图把 Session ID 放 query string 或 header 里替代 Cookie —— 协议层就不支持,重写中间件代价远超收益
开发时最容易忽略的三个细节
一是 maxAge 传负数删 Cookie 时,值必须是 -1,不是 "-1" 或 time.Second * -1;二是 Redis Store 初始化失败时,gin-contrib/sessions 不报错也不 panic,而是静默 fallback 到内存 store,导致上线后多实例不共享却查不出问题;三是 SameSite 参数在 c.SetCookie() 中是第七个 bool 参数,但 Gin v1.9+ 已支持 SameSite 枚举(如 http.SameSiteLaxMode),若用旧版 bool 传参,true 对应 Lax,false 对应 Default —— 这个隐式映射常被忽略。


















