beego Session 模块(server/web/session)

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

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

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

接口定义(session.go)

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

type Provider interface {                    <span>// :58-66</span>
    <span>SessionInit</span>(ctx, gcLifetime, config) error   # 引擎初始化(连接串)
    <span>SessionRead</span>(sid) (Store, error)
    <span>SessionExist</span>(sid) (bool, error)
    <span>SessionRegenerate</span>(oldSID, newSID) (Store, error)
    <span>SessionDestroy</span>(sid) error
    <span>SessionAll</span>() int
    <span>SessionGC</span>(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 <span>struct</span> {
    provider Provider
    config   *ManagerConfig     <span>// SessionName/CookieLifeTime/Gclifetime/Domain/SameSite...</span>
}

NewManager(provideName, cf)(109-144):查 provides、SessionInit(provider)、校验 header 模式 MIME。web 侧 hooks.go registerSession(47-77):按 BConfig 构建 ManagerConfig → 创建 GlobalSessionsgo 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.<span>SessionRead</span>(sid)
  └─ 无效:crypto/rand 生成 <span>16</span> 字节 <span>hex</span>(SessionIDPrefix 前缀,<span>345</span>-<span>352</span>)
          provider.<span>SessionRead</span>(new sid)
          写 cookie:<span>HttpOnly</span>(!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) <span>GC</span>() {
    manager<span>.provider</span><span>.SessionGC</span>(context.Background())
    <span>time</span><span>.AfterFunc</span>(manager.config.Gclifetime, func() { manager<span>.GC</span>() })
}

不用常驻 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;密钥=<span>hash</span>(session 安全串))
  → <span>base64</span>
  → 编码 cookie: name|<span>date</span>|value| 三段 <span>base64</span> + HMAC-SHA256 签名(encodeCookie,118-152)
读取:decodeCookie(154-188)
  → 校验 MAC(防篡改)
  → 校验 <span>date</span> 在 [now-gcmaxlifetime, now](防重放旧值)
  → 解 <span>base64</span> → AES 解密 → gob 解码

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

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

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

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

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

步骤 <span>7</span><span>:FindRouter</span> 预判 sessionOn(路由级开关)
        <span>GlobalSessions</span>.<span>SessionStart</span> → ctx.<span>Input</span>.<span>CruSession</span>
        defer <span>SessionReleaseIfPresent</span>
控制器/过滤器<span>:Get/Set</span>(内存 values)
步骤 <span>17</span> 后<span>:defer</span> 触发 <span>SessionRelease</span> → 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 历史比读代码更能理解它为什么存在。