3. Cookie 与 Session:无状态 HTTP 的身份凭证
HTTP 协议本身是无状态的——服务器无法区分两次请求是否来自同一用户。Cookie 与 Session 正是为解决这一矛盾而诞生的经典组合:Cookie 在客户端保存标识,Session 在服务端存储状态数据。本章将深入剖析两者的原理,并给出 Go 语言中的完整操作实现。
3.1 Cookie 原理
Cookie 是服务器通过 HTTP 响应头 Set-Cookie 下发给浏览器的一小段文本(通常 ≤ 4KB)。浏览器在后续请求中自动携带该 Cookie,使服务器能够识别用户身份或维持会话。
3.1.1 Cookie 核心属性
| 属性 | 说明 | 示例值 |
|---|---|---|
| Name | Cookie 名称,必须唯一 | session_id |
| Value | 存储的值,建议使用无意义标识符 | abc123xyz |
| Domain | 可访问该 Cookie 的域名 | .example.com |
| Path | 可访问该 Cookie 的路径 | / |
| Expires / Max-Age | 过期时间(绝对/相对秒数) | Session 或 3600 |
| Secure | 仅通过 HTTPS 传输 | 布尔值 |
| HttpOnly | 禁止 JavaScript 读取(防 XSS) | 布尔值 |
| SameSite | 跨站请求策略(防 CSRF) | Lax / Strict / None |
安全建议:生产环境务必设置
HttpOnly 和 Secure 为 true,SameSite 至少设为 Lax,避免敏感信息被脚本窃取或跨站利用。
3.1.2 Cookie 生命周期流程
3.2 Session 原理
Session 是服务端为每个用户创建的临时数据存储区。服务器生成一个唯一的 Session ID,通过 Cookie 下发给客户端;客户端后续请求携带该 ID,服务器据此查找对应的 Session 数据。关键区别在于:敏感数据保存在服务端,客户端只持有无意义的标识符。
3.2.1 Session 与 Cookie 对比
| 维度 | Cookie | Session |
|---|---|---|
| 数据存储位置 | 客户端(浏览器) | 服务端(内存 / Redis / 数据库) |
| 存储容量 | ≤ 4KB | 取决于服务端资源 |
| 安全性 | 易被篡改或窃取 | 数据不暴露给客户端 |
| 跨域支持 | 受同源策略限制 | 可通过共享存储实现 |
| 典型用途 | 记住偏好、跟踪行为 | 登录状态、购物车 |
3.3 Go 中 Cookie 操作
Go 标准库 net/http 提供了完整的 Cookie 读写接口。以下演示如何在 HTTP 处理器中设置、读取和删除 Cookie。
3.3.1 设置 Cookie
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "session_id",
Value: "abc123xyz",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 3600, // 1 小时后过期
}
http.SetCookie(w, cookie)
w.Write([]byte("Cookie 已设置"))
}
3.3.2 读取 Cookie
func readCookieHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "未找到 Cookie", http.StatusBadRequest)
return
}
w.Write([]byte("Cookie 值: " + cookie.Value))
}
3.3.3 删除 Cookie
func deleteCookieHandler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
MaxAge: -1, // 立即过期
}
http.SetCookie(w, cookie)
w.Write([]byte("Cookie 已删除"))
}
常见陷阱:删除 Cookie 时必须设置
MaxAge: -1 或 Expires 为过去时间,且 Path 和 Domain 必须与设置时完全一致,否则浏览器不会清除。
3.4 Session 实现与存储
Go 标准库未内置 Session 管理,但我们可以基于 sync.Map 或第三方存储(Redis)快速实现。以下给出一个内存版 Session 管理器,并对比不同存储方案的优劣。
3.4.1 内存版 Session 管理器
package session
import (
"crypto/rand"
"encoding/hex"
"sync"
"time"
)
type Session struct {
ID string
Data map[string]interface{}
ExpiresAt time.Time
}
type Manager struct {
mu sync.RWMutex
sessions map[string]*Session
cookieName string
maxAge int // 秒
}
func NewManager(cookieName string, maxAge int) *Manager {
return &Manager{
sessions: make(map[string]*Session),
cookieName: cookieName,
maxAge: maxAge,
}
}
func (m *Manager) generateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func (m *Manager) GetOrCreate(r *http.Request, w http.ResponseWriter) *Session {
m.mu.Lock()
defer m.mu.Unlock()
// 尝试从 Cookie 获取 Session ID
cookie, err := r.Cookie(m.cookieName)
if err == nil {
if s, ok := m.sessions[cookie.Value]; ok {
if s.ExpiresAt.After(time.Now()) {
return s
}
delete(m.sessions, cookie.Value) // 过期清理
}
}
// 创建新 Session
id := m.generateID()
s := &Session{
ID: id,
Data: make(map[string]interface{}),
ExpiresAt: time.Now().Add(time.Duration(m.maxAge) * time.Second),
}
m.sessions[id] = s
http.SetCookie(w, &http.Cookie{
Name: m.cookieName,
Value: id,
Path: "/",
HttpOnly: true,
Secure: true,
MaxAge: m.maxAge,
})
return s
}
func (m *Manager) Destroy(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(m.cookieName)
if err == nil {
m.mu.Lock()
delete(m.sessions, cookie.Value)
m.mu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: m.cookieName,
Value: "",
Path: "/",
MaxAge: -1,
})
}
3.4.2 存储方案对比
| 存储方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 内存 (sync.Map) | 极快、零依赖 | 重启丢失、无法水平扩展 | 单机开发 / 低并发原型 |
| Redis | 持久化、支持 TTL、集群扩展 | 额外运维成本 | 生产环境、多实例部署 |
| 数据库 (MySQL/PostgreSQL) | 持久化、可审计 | 读写延迟高 | 需要持久化 Session 日志 |
| Cookie 自包含 (JWT) | 无状态、无需服务端存储 | 无法主动失效、载荷大小受限 | 分布式无状态认证 |
3.4.3 Session 存储架构图
3.5 实战:在 Gin 框架中集成 Session
以 Gin 为例,演示如何将上述 Session 管理器嵌入中间件,实现用户登录状态的保持。
package main
import (
"github.com/gin-gonic/gin"
"your-project/session"
)
var sm = session.NewManager("sid", 3600)
func main() {
r := gin.Default()
// 登录接口
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
// 验证用户名密码(省略)
if username != "admin" || password != "123456" {
c.JSON(401, gin.H{"error": "认证失败"})
return
}
s := sm.GetOrCreate(c.Request, c.Writer)
s.Data["username"] = username
s.Data["role"] = "admin"
c.JSON(200, gin.H{"message": "登录成功"})
})
// 需要认证的接口
r.GET("/profile", func(c *gin.Context) {
s := sm.GetOrCreate(c.Request, c.Writer)
username, ok := s.Data["username"].(string)
if !ok {
c.JSON(401, gin.H{"error": "未登录"})
return
}
c.JSON(200, gin.H{"username": username, "role": s.Data["role"]})
})
// 登出接口
r.POST("/logout", func(c *gin.Context) {
sm.Destroy(c.Writer, c.Request)
c.JSON(200, gin.H{"message": "已登出"})
})
r.Run(":8080")
}
关键要点:
- Session ID 必须使用加密随机数生成,避免被预测。
- 每次请求都需检查 Session 是否过期,过期后自动清理。
- 生产环境务必使用 Redis 等外部存储,支持多实例共享 Session。
- Cookie 的
HttpOnly和Secure是安全底线,不可省略。
3.6 安全风险与防御
| 攻击类型 | 描述 | 防御措施 |
|---|---|---|
| Session 劫持 | 攻击者窃取 Cookie 冒充用户 | 启用 HTTPS、设置 Secure 标志、绑定 IP/User-Agent |
| Session 固定 | 攻击者诱导用户使用已知 Session ID | 登录后重新生成 Session ID |
| XSS 窃取 | 通过脚本读取 Cookie | 设置 HttpOnly 标志 |
| CSRF 利用 | 跨站请求伪造 | 设置 SameSite 为 Lax/Strict、使用 CSRF Token |
最佳实践:每次用户权限变更(如登录、提权)时,务必调用
Destroy 并重新生成 Session,防止 Session 固定攻击。