build-web-application-with-golang 第 6.1 节精读:Cookie 与 Session 原理及 Go 实战

原创2026-10-04 15:59:081,355 阅读
文章标签:文档教程

build-web-application-with-golang 第 6.1 节精读:Cookie 与 Session 原理及 Go 实战

导读

HTTP 是无状态协议,服务器无法天然记住"上一个请求是否已通过登录验证"。本篇文章基于开源项目 build-web-application-with-golang 的第 6.1 节(pt-br/06.1.md),系统讲解 Web 开发中最基础也最容易混淆的两个概念——Cookie 与 Session:它们分别解决了什么、如何配合、在 Go 的 net/http 标准库中如何设置与读取,以及它们各自的安全弱点。读完后你将掌握:用 http.SetCookie 下发 Cookie、用 r.Cookie / r.Cookies 读取 Cookie,并理解 Session 为何必须以"唯一且随机"的 session id 作为客户端标识,为后续章节实现自己的 Session 管理器打下基础。

HTTP 的无状态困境:为什么需要 Cookie 与 Session

设想你要用程序(而不是人肉浏览器)爬取一个需要登录才能访问的页面,例如某个 Twitter 用户的主页。浏览器里你只需输入用户名和密码,点击"登录",浏览器就会向服务器发送 POST 请求;服务器校验通过后返回响应并跳转到用户主页。这里的关键问题是:服务器如何知道你现在有权限访问这个页面?

因为 HTTP 是无状态的,服务器无法得知你在"上一步"是否通过了校验。原文档给出了两种朴素思路:

  1. 把用户名和密码拼进 URL——虽然可行,但服务器必须在每次请求时都到数据库里重新校验一次,压力巨大,用户体验也很差;
  2. 借助 Cookie 和 Session 保存用户身份——把身份信息保存在服务端或客户端,这正是现代 Web 的标准做法。

第 6 章开篇(pt-br/06.0.md)进一步点明了全章脉络:Cookie 是客户端侧机制,Session 保存在服务端并给每个用户一个唯一标识;session id 可以通过 URL、Cookie 甚至数据库传递(存数据库更安全,但可能拖累性能)。

Cookie:存储在客户端的"历史信息"

Cookie 的要点可以概括为一句话:把历史信息(包括用户登录信息)保存在客户端的计算机上。浏览器在每次访问同一网站时自动携带这些 Cookie,从而替用户自动完成"登录"这一步。

下图展示了 Cookie 的工作原理:客户端第一次请求时,服务器页面 1 为客户端设置 Cookie 值;此后客户端再次访问服务器页面 2 时,请求会自动带上该 Cookie,服务器据此识别客户端身份。

Cookie 工作原理:客户端首次请求时服务器写入 Cookie,后续请求自动携带

Cookie 的生命周期:会话 Cookie 与持久化 Cookie

Cookie 都有过期时间,依据生命周期可以分成两类:

  • 会话 Cookie(session cookies):应用不设置过期时间时,浏览器关闭后不会把 Cookie 写入本地文件系统,而是保存在内存中。这类 Cookie 随浏览器进程存活而存在,关闭即失效。
  • 持久化 Cookie(persistent cookies):应用设置了过期时间(例如 setMaxAge(60*60*24),在 Go 中对应 Cookie.MaxAge 字段),浏览器会把 Cookie 写入本地文件系统,直到到达设定的过期时间才删除。

两者还有一个差异值得注意:写入本地文件系统的 Cookie 可以被不同的浏览器进程共享(例如两个 IE 窗口);而保存在内存中的 Cookie 由各个浏览器进程各自处理,互不共享。你可以在浏览器的 Cookie 管理界面中查看这些已保存的 Cookie 数据:

浏览器中的 Cookie 数据管理界面,可按站点查看已存储的 Cookie 及删除操作

Session:存储在服务端的"会话状态"

Session 与 Cookie 相对,把历史信息保存在服务端。它的核心机制是:

  • 服务器为每个客户端生成一个 session id,用它来标识不同的会话;
  • 这个 session id 必须始终是随机且唯一的;
  • 客户端通过 Cookie 或 URL 参数把自己的身份(session id)带给服务器。

下图展示了 Session 的原理:客户端首次访问时,服务器建立 session id 与会话数据的关联(步骤 1);后续请求携带 session id,服务器据此找到对应的会话数据完成交互(步骤 2、3)。会话数据留在服务端,客户端只保存一个"钥匙"。

Session 工作原理:服务端按 session id 存储会话数据,客户端仅携带该标识

从会话管理的视角看,Session 是一系列动作或消息的集合(可以类比"拿起电话到挂断电话"之间的一通电话),在网络协议语境下更多指浏览器与服务器之间的连接状态。Session 是服务端机制,通常用哈希表(或类似数据结构)保存到达的信息。当应用需要为客户端分配新 Session 时,服务器会先检查该客户端是否已存在唯一的 session id:

  • 已存在:直接把同一个 Session 返回给客户端;
  • 不存在:创建全新的 Session(这种情况通常发生在服务端已删除对应 session id、而用户手动把旧的 session id 又带了回来的场景)。

原文档特别强调:"Session 本身并不复杂,但它的实现与部署很复杂,不存在'一招通吃'的方案。"这也是后续章节要自研 Session 管理器的原因——Go 标准库目前并没有内置 Session 支持。

在 Go 中设置 Cookie

Go 通过 net/http 包中的 SetCookie 函数下发 Cookie:

http.SetCookie(w ResponseWriter, cookie *Cookie)

其中 w 是当前请求的响应,cookie 是一个 Cookie 结构体。该结构体的完整定义如下(字段含义即注释所写):

type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string

// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // Raw text of unparsed attribute-value pairs
}

各字段的作用可以归纳为:

  • Name / Value:Cookie 的键值对,是最核心的信息载体;
  • Path / Domain:限定 Cookie 的生效范围(哪些路径、哪些域名下的请求会携带它);
  • Expires / MaxAge:控制过期时机。MaxAge 语义特殊:等于 0 表示不指定 Max-Age 属性;小于 0 表示立即删除(等价于 Max-Age: 0);大于 0 表示 Max-Age 属性存在,单位是秒;
  • Secure:为 true 时 Cookie 仅在 HTTPS 连接中传输;
  • HttpOnly:为 true 时禁止客户端脚本(JavaScript)通过 document.cookie 读取该 Cookie,是防御 XSS 窃取会话的关键开关(后续 6.4 节会重点用到)。

设置一个有效期为 1 年的 Cookie 的完整示例:

expiration := time.Now().Add(365 * 24 * time.Hour)
cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration}
http.SetCookie(w, &cookie)

这段代码会通过响应头的 Set-Cookie 字段把名为 username、值为 astaxie 的 Cookie 下发给浏览器。

在 Go 中获取 Cookie

读取客户端携带过来的 Cookie 同样简单。按名字取单个 Cookie:

cookie, _ := r.Cookie("username")
fmt.Fprint(w, cookie)

或者遍历请求中的所有 Cookie,打印每个 Cookie 的名字:

for _, cookie := range r.Cookies() {
    fmt.Fprint(w, cookie.Name)
}

r.Cookie(name) 在找不到对应 Cookie 时会返回 http.ErrNoCookie 错误(上面的示例忽略了错误处理,实际代码中建议显式判断);r.Cookies() 则返回本次请求携带的全部 *http.Cookie 切片。两个 API 都位于 net/http 标准库的 Request 类型上,使用起来非常便捷。

Session 与 Cookie 的差异及安全风险

原文档在 Summary 中给出了二者的本质对比:

对比项 Cookie Session
存储位置 客户端(浏览器) 服务端(内存、文件或数据库)
客户端保存内容 全部信息(含用户名、密码等) 仅一个 session id(通常放在 Cookie 或 URL 中)
解决的核心问题 克服 HTTP 无状态 克服 HTTP 无状态
安全弱点 信息明文暴露在客户端,易被窃取 依赖 session id 的保密性与随机性

一句话概括:两者目的相同(都是为了克服 HTTP 的无状态性),但实现方式不同——Session 用 Cookie 在客户端保存 session id,把其余所有信息留在服务端;Cookie 则把所有客户端信息都保存在客户端。

由于 Cookie 明文保存在客户端,它天然存在安全隐患:用户名和密码等信息可能被恶意第三方网站破解并收集。原文档列举了两个常见的攻击场景:

  1. 跨站设置 Cookie:appA 给 appB 设置一个意想不到的 Cookie;
  2. XSS 攻击:appA 利用 JavaScript 的 document.cookie 读取 appB 的 Cookie。

这正是为什么会话类敏感信息应放入服务端 Session,且会话 Cookie 必须开启 HttpOnly 并保证 session id 随机性的原因。

从本节到 Session 管理器:仓库中的下一步

6.1 节只解决了"概念"问题,动手实现则从下一节开始。仓库中 pt-br/06.2.md 会基于本节的设计思想,手把手实现一个 Go Session 管理器,其中与本节点题直接相关的设计包括:

  • Provider 接口:抽象 Session 的底层存储(内存、文件、数据库),定义 SessionInit、SessionRead、SessionDestroy、SessionGC 四个方法;
  • Session 接口:只保留四个操作——Set(设值)、Get(取值)、Delete(删值)、SessionID()(取当前 session id);
  • 唯一 session id 的生成:用 crypto/rand 读取 32 字节随机数再经 base64.URLEncoding 编码,保证随机且唯一;
  • SessionStart 的"先查后建"逻辑:先尝试从请求 Cookie 中读取 session id(对应本节的 r.Cookie),没有则生成新 id 并通过 http.SetCookie 下发(对应本节的 http.SetCookie),与 6.1 节的两个 API 形成完整的实战闭环;
  • SessionDestroy 的登出清理:销毁服务端会话的同时,用 MaxAge: -1 的 Cookie 让浏览器立即删除会话 Cookie,正是本节 Cookie 结构体中 MaxAge<0 语义的实际运用。

再往后,pt-br/06.4.md 专门讨论会话劫持防护:仅允许 session id 走 Cookie(不用 URL 重写)、开启 HttpOnly 阻断 XSS 读取、为每个请求附加 token 校验、以及定期更换 session id 并设置超时。这些防护措施的技术根基,都在本节的 Cookie 结构体字段与 Session 机制之中。

小结

经过本节的学习,你应当掌握:Cookie 是存储在客户端的历史信息载体,按是否设置过期时间可分为会话 Cookie 与持久化 Cookie;Session 是存储在服务端的会话状态,靠随机且唯一的 session id 与客户端建立关联;在 Go 中通过 http.SetCookie 设置 Cookie、通过 r.Cookie / r.Cookies 读取 Cookie;同时理解二者目的相同而实现不同,并清楚 Cookie 明文存储带来的两类常见攻击。接下来就可以进入 pt-br/06.2.md,用 Go 从零实现一个属于自己的 Session 管理器。

登录后查看全文
build-web-application-with-golang