3. Cookie 与 Session:无状态 HTTP 的身份凭证

HTTP 协议本身是无状态的——服务器无法区分两次请求是否来自同一用户。Cookie 与 Session 正是为解决这一矛盾而诞生的经典组合:Cookie 在客户端保存标识,Session 在服务端存储状态数据。本章将深入剖析两者的原理,并给出 Go 语言中的完整操作实现。

3.1 Cookie 原理

Cookie 是服务器通过 HTTP 响应头 Set-Cookie 下发给浏览器的一小段文本(通常 ≤ 4KB)。浏览器在后续请求中自动携带该 Cookie,使服务器能够识别用户身份或维持会话。

3.1.1 Cookie 核心属性

属性说明示例值
NameCookie 名称,必须唯一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 生命周期流程

Cookie 生命周期流程 浏览器 服务器 ① 请求资源 ② Set-Cookie: session_id=abc123 存储 Cookie ③ 后续请求自动携带 Cookie 验证 Cookie ④ 返回个性化内容

3.2 Session 原理

Session 是服务端为每个用户创建的临时数据存储区。服务器生成一个唯一的 Session ID,通过 Cookie 下发给客户端;客户端后续请求携带该 ID,服务器据此查找对应的 Session 数据。关键区别在于:敏感数据保存在服务端,客户端只持有无意义的标识符。

3.2.1 Session 与 Cookie 对比

维度CookieSession
数据存储位置客户端(浏览器)服务端(内存 / 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 存储架构图

Session 存储架构方案 客户端 应用服务器 请求 + Cookie 内存 (sync.Map) Redis 数据库 选择建议 • 开发/测试 → 内存 • 生产单机 → Redis • 分布式集群 → Redis Cluster

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 固定攻击。