beego Session 模块(server/web/session)

文章来源声明: 原文作者:H_拾忆; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684658712821596187; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

beego Session 设计清晰,引擎可插拔,sid 多通道适合 App 客户端。但写延迟语义下并发需注意,推荐 redis 引擎保证强一致。本文是理解 beego 会话机制与生产避坑的优质源码解读。

1. 三级架构 -------
Manager(每应用一个,GlobalSessions)
   │ 持有 provider(按配置选择的引擎)
   ▼
Provider(每引擎一个,"会话仓库")
   │ SessionRead(sid) 创建/取出
   ▼
Store(每请求一个)
   │ Get/Set/Delete/Flush…
   ▼
SessionRelease(请求结束,写回仓库)

接口定义(session.go)

type Store interface {                       // :46-54
    Set(ctx, key, val) error
    Get(ctx, key) interface{}
    Delete(ctx, key) error
    SessionID(ctx) string
    SessionReleaseIfPresent(ctx, w)          # 2.x 新增:防御性释放
    SessionRelease(ctx, w)                   # 写回持久层
    Flush(ctx) error                         # 清空
}

type Provider interface {                    // :58-66
    SessionInit(ctx, gcLifetime, config) error   # 引擎初始化(连接串)
    SessionRead(sid) (Store, error)
    SessionExist(sid) (bool, error)
    SessionRegenerate(oldSID, newSID) (Store, error)
    SessionDestroy(sid) error
    SessionAll() int
    SessionGC(ctx)                           # 引擎各自的清理
}

注册表(session.go:75-89)

provides map[string]Provider + Register(name, provide);12 个引擎各自 init 注册:cookie(file: sess_cookie.go)、file、mem、redis、redis_cluster、redis_sentinel、memcache、mysql、postgres、couchbase、ledis、ssdb。

  1. Manager(session.go:92-144)

type Manager struct {
    provider Provider
    config   *ManagerConfig     // SessionName/CookieLifeTime/Gclifetime/Domain/SameSite...
}

NewManager(provideName, cf)(109-144):查 provides、SessionInit(provider)、校验 header 模式 MIME。web 侧 hooks.go registerSession(47-77):按 BConfig 构建 ManagerConfig → 创建 GlobalSessions → go GlobalSessions.GC()。

2.1 sid 从哪来:getSid(158-184)

三级回退:Cookie → URL query → HTTP header(后两者需 SessionEnableSidInURLQuery/HeaderIP 开启)。这就是"App 客户端无 cookie 也能带 session"的实现。

2.2 SessionStart(188-238)——每请求入口

getSid 取 sid
  ├─ 有效:provider.SessionRead(sid)
  └─ 无效:crypto/rand 生成 16 字节 hex(SessionIDPrefix 前缀,345-352)
          provider.SessionRead(new sid)
          写 cookie:HttpOnly(!SessionDisableHTTPOnly)/Secure/Domain/SameSite/CookieLifeTime
          (SessionEnableSidInHTTPHeader 时也写响应头)
返回 Store → serveHttp 放入 ctx.Input.CruSession

2.3 SessionRegenerateID(284-333)

换 sid 保数据:provider.SessionRegenerate(oldsid, newsid) + 重写 cookie——防会话固定攻击的服务端支持。

2.4 GC 自调度(278-281)——优雅的技巧

func (manager *Manager) GC() {
    manager.provider.SessionGC(context.Background())
    time.AfterFunc(manager.config.Gclifetime, func() { manager.GC() })
}

不用常驻 ticker,每次执行完后安排下一次:改 Gclifetime 配置能在下轮生效,且无泄漏。值得抄的模式。

2.5 会话释放的演进

  • v2.0:defer SessionRelease(ctx, w)

  • 近期修复(git log: "use request context for session release")新增 SessionReleaseIfPresent:以请求 context 为准,请求 ctx 已取消时也能安全释放,避免客户端断连导致会话丢写。各引擎同步实现。

  1. cookie 引擎:客户端会话的安全样本(sess_cookie.go + sess_utils.go)

存储介质是 cookie 本身(4KB 限制),数据链:

写入:map[string]interface{}
  → gob 编码(EncodeGob,47-70)
  → AES-CTR 加密(encrypt,87-116;密钥=hash(session 安全串))
  → base64
  → 编码 cookie: name|date|value| 三段 base64 + HMAC-SHA256 签名(encodeCookie,118-152)
读取:decodeCookie(154-188)
  → 校验 MAC(防篡改)
  → 校验 date 在 [now-gcmaxlifetime, now](防重放旧值)
  → 解 base64 → AES 解密 → gob 解码

为什么三层:签名防篡改、加密防偷看(明文 key)、时间戳防重放。虽然单值 cookie 一般不建议存敏感数据,但这套编码是 beego 自身 SecureCookie(context 包)的同源实现。

  1. file 引擎走读(sess_file.go)——引擎范本

type FileSessionStore struct { sid; lock sync.RWMutex; values map[...]; ... }
func (fs *FileSessionStore) SessionRelease(ctx, w) {
    // gob 编码 values → 写回 sid 文件(带文件锁)
}
type FileProvider struct { maxlifetime; savePath }
func (fp *FileProvider) SessionRead(sid) {
    // 打开/创建 savePath/sid,独占锁;空文件初始化
}
func (fp *FileProvider) SessionGC(ctx) {
    // filepath.Walk:按 ModTime > maxlifetime 删除过期文件(gcpath,302)
}

redis/mysql 引擎同构:ProviderConfig = 连接串,SessionRead 建连接取 hash/行,SessionRelease 写回,GC 用 EXPIRE/DELETE。读引擎源码的套路:只看 SessionInit/Read/Release/GC 四个方法。

  1. 与 serveHttp 的协作(S05 回顾)

步骤 7:FindRouter 预判 sessionOn(路由级开关)
        GlobalSessions.SessionStart → ctx.Input.CruSession
        defer SessionReleaseIfPresent
控制器/过滤器:Get/Set(内存 values)
步骤 17 后:defer 触发 SessionRelease → provider 写回

写延迟语义:会话修改在请求结束才持久化——意味着"同一用户并发两请求"存在丢失更新(file 引擎整文件覆盖)。需要强一致计数请用 redis 引擎的原子命令或 DB 行锁。

  1. 内存引擎的 map 锁(sess_mem.go)

CookieSessionStore/mem 引擎:RWMutex 保护 values;SessionAll 遍历计数。单机开发够用,多副本部署必换外部引擎(使用篇 Day08 坑 2 的根源)。

  1. 设计赏析

  1. 三级接口职责清晰:Manager(策略/sid/cookie)、Provider(仓储)、Store(读写);新引擎 4-7 个方法搞定。

  2. GC 的 AfterFunc 自调度:见 2.4。

  3. sid 多通道(cookie/query/header):移动端友好,代价是 XSS/URL 泄漏面变大——默认只开 cookie。

  4. 防御性释放(SessionReleaseIfPresent):一个真实生产 bug 催化的 API 演进,读 git 历史比读代码更能理解它为什么存在。