会话管理
SessionManager 提供线程安全的 Cookie 和请求头存储,由 DomainClient 内部嵌入使用。它封装了一套基于 sync.RWMutex 的并发安全存储,所有读取操作使用读锁、写入操作使用写锁,适合在高并发场景下跨请求共享会话状态。
何时需要直接使用 SessionManager
通常你无需手动创建 SessionManager——NewDomain 创建的 DomainClient 会自动嵌入一个。直接使用 SessionManager 的场景包括:需要在多个 DomainClient 之间共享会话、需要运行时切换 Cookie 安全策略、或者需要从响应中批量提取 Cookie。
SessionConfig
type SessionConfig struct {
// CookieSecurity 配置 Cookie 安全验证。
// nil 表示不进行 Cookie 安全验证。
CookieSecurity *CookieSecurityConfig
}| 字段 | 类型 | 说明 |
|---|---|---|
CookieSecurity | *CookieSecurityConfig | Cookie 安全验证配置,nil 表示不验证 |
func DefaultSessionConfig() SessionConfig返回默认配置(不进行 Cookie 安全验证)。使用方式见下方 NewSessionManager。
NewSessionManager
func NewSessionManager(cfg SessionConfig) (*SessionManager, error)创建会话管理器。传入 SessionConfig 值,或使用 NewSessionManagerDefault() 作为零参快捷方式。当前实现始终返回 nil error,error 返回值保留用于未来扩展的配置校验。
// 使用默认配置
sm, err := httpc.NewSessionManagerDefault()
// 带配置(启用严格 Cookie 安全验证)
cfg := httpc.DefaultSessionConfig()
cfg.CookieSecurity = httpc.StrictCookieSecurityConfig()
sm, err := httpc.NewSessionManager(cfg)NewSessionManager vs NewSessionManagerDefault
| 构造函数 | 参数 | 适用场景 |
|---|---|---|
NewSessionManager(cfg) | 显式 SessionConfig | 需要自定义 CookieSecurity 策略 |
NewSessionManagerDefault() | 无 | 默认配置(无 Cookie 安全验证) |
NewSessionManagerDefault() 等价于 NewSessionManager(DefaultSessionConfig()),与主客户端的 NewDefault() 设计对称。
NewSessionManagerDefault
func NewSessionManagerDefault() (*SessionManager, error)便捷构造,等价于 NewSessionManager(DefaultSessionConfig())。
sm, err := httpc.NewSessionManagerDefault()头管理
SessionManager 通过以下方法维护跨请求的请求头。所有方法均为线程安全,写入操作(SetHeader/SetHeaders/DeleteHeader/ClearHeaders)获取写锁,读取操作(GetHeaders)获取读锁。
SetHeader
func (s *SessionManager) SetHeader(key, value string) error添加或更新单个会话头。所有后续请求自动携带。调用前通过 validation.ValidateHeaderKeyValue 校验键值合法性(拦截控制字符、CRLF 注入等),无效时返回包装后的错误。nil 接收者返回 "session manager is nil"。
err := sm.SetHeader("Authorization", "Bearer "+token)
if err != nil {
log.Fatalf("设置请求头失败: %v", err)
}SetHeaders
func (s *SessionManager) SetHeaders(headers map[string]string) error批量添加或更新会话头。先在锁外逐项校验(任一无效即整批拒绝),再在锁内通过 maps.Copy 合并。原子语义:要么全部成功,要么全部不变。
err := sm.SetHeaders(map[string]string{
"Authorization": "Bearer " + token,
"Accept": "application/json",
"X-Custom": "value",
})DeleteHeader
func (s *SessionManager) DeleteHeader(key string)按 key 删除指定会话头。key 不存在时静默无操作。nil 接收者安全(直接返回)。
ClearHeaders
func (s *SessionManager) ClearHeaders()清空所有会话头,重新初始化为空 map。
GetHeaders
func (s *SessionManager) GetHeaders() map[string]string返回所有会话头的深拷贝副本(新分配 map + 值拷贝),调用方修改副本不影响内部状态。空会话返回空 map(非 nil)。
头管理方法一览
| 方法 | 签名 | 锁 | 说明 |
|---|---|---|---|
| SetHeader | (key, value string) error | 写锁 | 单个设置,含校验 |
| SetHeaders | (headers map[string]string) error | 写锁 | 批量设置,原子校验 |
| DeleteHeader | (key string) | 写锁 | 按 key 删除 |
| ClearHeaders | () | 写锁 | 清空全部 |
| GetHeaders | () map[string]string | 读锁 | 返回深拷贝副本 |
Cookie 管理
SessionManager 通过以下方法维护跨请求的 Cookie。所有写入操作均通过 validation.ValidateCookie 校验合法性;若配置了 CookieSecurity,还会额外校验安全属性(Secure/HttpOnly/SameSite)。
SetCookie
func (s *SessionManager) SetCookie(cookie *http.Cookie) error添加或更新单个会话 Cookie。校验流程:① nil cookie 检查;②基础格式校验;③ 若配置了 CookieSecurity,执行 validateCookieSecurity(在写锁内执行以保证线程安全)。任一校验失败返回包装错误,不修改存储。
err := sm.SetCookie(&http.Cookie{
Name: "session",
Value: "abc123",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
if err != nil {
log.Fatalf("设置 Cookie 失败: %v", err)
}SetCookies
func (s *SessionManager) SetCookies(cookies []*http.Cookie) error批量设置 Cookie。采用两阶段原子写入:① 锁外预校验所有 cookie 的基础合法性(nil 检查 + ValidateCookie);② 锁内逐项执行安全校验,任一失败立即返回错误(已校验的也不会写入);③ 全部通过后才统一存储。这保证了批量操作的原子性——不会出现"部分写入"的中间状态。
DeleteCookie
func (s *SessionManager) DeleteCookie(name string)按名称删除 Cookie。name 不存在时静默无操作。
ClearCookies
func (s *SessionManager) ClearCookies()清空所有 Cookie,重新初始化为空 map。
GetCookies
func (s *SessionManager) GetCookies() []*http.Cookie返回所有 Cookie 的深拷贝副本。采用连续 backing array 优化:预分配一个长度为 N 的 []http.Cookie 连续数组,所有 Cookie 结构体在其中连续排列,返回的 []*http.Cookie 指向该数组。这将 N 次独立堆分配降为 2 次(backing array + 指针切片),显著降低 GC 压力。空会话返回 nil。
GetCookie
func (s *SessionManager) GetCookie(name string) *http.Cookie按名称获取 Cookie 的深拷贝副本,不存在返回 nil。
Cookie 管理方法一览
| 方法 | 签名 | 锁 | 校验 |
|---|---|---|---|
| SetCookie | (cookie *http.Cookie) error | 写锁 | ValidateCookie + 可选 CookieSecurity |
| SetCookies | (cookies []*http.Cookie) error | 写锁 | 两阶段原子校验 |
| DeleteCookie | (name string) | 写锁 | 无 |
| ClearCookies | () | 写锁 | 无 |
| GetCookies | () []*http.Cookie | 读锁 | 无(返回深拷贝) |
| GetCookie | (name string) *http.Cookie | 读锁 | 无(返回深拷贝) |
Cookie 安全
SetCookieSecurity
func (s *SessionManager) SetCookieSecurity(config *CookieSecurityConfig)运行时更新 Cookie 安全验证配置,影响后续所有 SetCookie/SetCookies/UpdateFromResult/UpdateFromCookies 调用。传 nil 可禁用安全验证。nil 接收者安全。这是切换安全策略的唯一入口——无需重建 SessionManager 即可在运行时从宽松切换到严格策略,或反之。
// 运行时从宽松切换到严格
sm.SetCookieSecurity(httpc.StrictCookieSecurityConfig())
// 禁用安全验证
sm.SetCookieSecurity(nil)CookieSecurityConfig 字段
type CookieSecurityConfig struct {
RequireSecure bool // 要求 Secure 属性(仅 HTTPS 传输)
RequireHttpOnly bool // 要求 HttpOnly 属性(防 XSS)
RequireSameSite string // 要求 SameSite 值:"Strict"/"Lax"/"None"/""
AllowSameSiteNone bool // 是否允许 SameSite=None
RequireSecureForSameSiteNone bool // SameSite=None 时强制要求 Secure
}可用工厂函数:
| 工厂函数 | 说明 |
|---|---|
DefaultCookieSecurityConfig() | 宽松默认(允许非 HTTPS、允许 JS 访问、允许 SameSite=None) |
StrictCookieSecurityConfig() | 严格策略(要求 Secure + HttpOnly + SameSite=Strict) |
UpdateFromResult
func (s *SessionManager) UpdateFromResult(result *Result)从请求结果(*Result)的 Response.Cookies 中提取 Cookie 并存入会话。若配置了 CookieSecurity,不安全的 Cookie 会被静默跳过(不返回错误,直接忽略),仅存储通过校验的 Cookie。result 为 nil、Response 为 nil 或 Cookies 为空时直接返回。DomainClient 的 Request 方法在每次请求后自动调用此方法。
UpdateFromCookies
func (s *SessionManager) UpdateFromCookies(cookies []*http.Cookie)从 Cookie 切片中更新会话 Cookie。语义与 UpdateFromResult 一致——不安全 Cookie 静默跳过。DomainClient 的 Download 方法通过此方法捕获下载响应中的 Cookie。
内部机制
captureFromOptions
func (s *SessionManager) captureFromOptions(options []RequestOption)DomainClient 的 prepareSessionOptions 内部调用此方法,从用户提供的 RequestOptions 中提取 Cookie 和 Header 存入会话。实现细节:
- 使用对象池中的临时
engine.Request(acquireMiddlewareRequest/releaseMiddlewareRequest)减少热路径分配 - 逐个应用 option 到临时请求——安全措施:每个 option 应用前后清除 OnRequest/OnResponse 回调,防止
WithOnRequest/WithOnResponse的闭包在捕获过程中累积触发副作用 - 提取临时请求上的 Cookie 和 Header,经校验后存入会话
- 只提取 Cookie 和 Header,其他数据(query params、body、callbacks)被丢弃
RequestOptions 执行两次
DomainClient 的 Request/Download 会将 RequestOptions 执行两次——一次用于会话捕获(captureFromOptions),一次用于实际请求。因此避免使用有副作用的 option(如计数器、nonce 生成、随机数)。如需副作用,请直接使用底层 Client。
prepareOptions
func (s *SessionManager) prepareOptions() []RequestOptionDomainClient 每次请求前调用此方法,将当前会话状态注入为 RequestOptions:
- Cookie 批量注入:将所有会话 Cookie 打包到一个闭包 option 中(
r.SetCookies(append(existing, cookies...))),避免 N 次闭包分配 - Header map 注入:通过
WithHeaderMap一次性注入深拷贝的 header map
会话为空(无 Cookie 且无 Header)时返回 nil,零开销。
线程安全模型
SessionManager 通过单个 sync.RWMutex 保护所有状态:
| 操作类型 | 锁级别 | 方法 |
|---|---|---|
| 读取(GetHeaders/GetCookies/GetCookie/prepareOptions) | RLock | 可并发 |
| 写入(Set*/Delete*/Clear*/UpdateFrom*/captureFromOptions/SetCookieSecurity) | Lock | 互斥 |
DomainClient 的 prepareSessionOptions 采用"先读后写"的非原子序列:先读快照(prepareOptions)再写捕获(captureFromOptions),两步之间可能有并发请求交错。这是最终一致性设计——每个请求在 prepareOptions() 时刻捕获一致快照,跨请求的瞬时竞态不影响单次请求的正确性。
完整示例:登录会话维持
以下示例展示 DomainClient 如何自动管理登录会话:登录后 Cookie 自动维持,直至登出。
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/cybergodev/httpc"
)
func main() {
ctx := context.Background()
// 创建 DomainClient,自动启用 Cookie 并嵌入 SessionManager
dc, err := httpc.NewDomain("https://httpbin.org", httpc.DefaultConfig())
if err != nil {
log.Fatalf("创建客户端失败: %v", err)
}
defer func() { _ = dc.Close() }()
// 手动设置会话头(后续所有请求自动携带)
sm := dc.Session()
if err := sm.SetHeader("Accept", "application/json"); err != nil {
log.Fatalf("设置请求头失败: %v", err)
}
if err := sm.SetCookie(&http.Cookie{
Name: "session",
Value: "initial",
}); err != nil {
log.Fatalf("设置 Cookie 失败: %v", err)
}
// 登录:响应的 Set-Cookie 自动由 UpdateFromResult 捕获到会话
loginCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
_, err = dc.Request(loginCtx, "POST", "/cookies/set?token=abc123")
cancel()
if err != nil {
log.Fatalf("登录失败: %v", err)
}
// 后续请求自动携带会话中的 Cookie
verifyCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
result, err := dc.Request(verifyCtx, "GET", "/cookies")
cancel()
if err != nil {
log.Fatalf("验证失败: %v", err)
}
fmt.Println("会话 Cookie 维持成功,响应:")
fmt.Println(result.String())
// 登出:清除会话
sm.ClearCookies()
sm.ClearHeaders()
fmt.Println("已登出,会话已清除")
}手动管理 SessionManager
你也可以独立创建 SessionManager 并在多个 DomainClient 之间共享。但通常 DomainClient 的自动管理已满足需求——每次请求后自动捕获响应 Cookie,请求前自动注入会话状态。