<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。
- 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 → 创建 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.<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 已取消时也能安全释放,避免客户端断连导致会话丢写。各引擎同步实现。
- 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 包)的同源实现。
- 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 四个方法。
- 与 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 行锁。
- 内存引擎的 map 锁(sess_mem.go)
CookieSessionStore/mem 引擎:RWMutex 保护 values;SessionAll 遍历计数。单机开发够用,多副本部署必换外部引擎(使用篇 Day08 坑 2 的根源)。
- 设计赏析
-
三级接口职责清晰:Manager(策略/sid/cookie)、Provider(仓储)、Store(读写);新引擎 4-7 个方法搞定。
-
GC 的 AfterFunc 自调度:见 2.4。
-
sid 多通道(cookie/query/header):移动端友好,代价是 XSS/URL 泄漏面变大——默认只开 cookie。
-
防御性释放(SessionReleaseIfPresent):一个真实生产 bug 催化的 API 演进,读 git 历史比读代码更能理解它为什么存在。
beego Session 设计清晰,引擎可插拔,sid 多通道适合 App 客户端。但写延迟语义下并发需注意,推荐 redis 引擎保证强一致。本文是理解 beego 会话机制与生产避坑的优质源码解读。