📚 本文收录于「流浪」的系列专栏
| 🐧Linux系统 | ⚙️C++ |
| 📊数据结构与算法 | 🐍Python |
| 🔗LangChain & LangGraph | 🗄️MySQL 数据库 |
| 🌿Git 工具 | 🌐计算机网络 |
| 🤖LLM | 💯大厂面试、八股 |
| 📚学习筑基专栏 |
🏠 博客主页:流浪 | 📝 原创首发于 CSDN
前言:《计算机网络篇12》讲完了方法与连接:参数怎么放、连接怎么复用都清楚了。可 HTTP 不记得你是谁,登录一次凭什么一直有效?这一篇补上无状态这个缺口——Cookie 在客户端存凭证,Session 在服务端存数据。
一、无状态:每个请求都被当成第一次见面
1.1 登录 B 站之后不再输密码,和「无状态」看着是矛盾的
假设 HTTP 真的不记人,那登录 B 站之后会发生什么:点一个视频是一次请求,点个赞是一次请求,发条评论又是一次请求,每一次服务器都该把你当陌生人,每一次都该让你重新输账号密码。现实里没有发生,登录一次能用到退出为止。
这就是本篇要解决的问题:如果协议不记状态,那「已登录」这件事存在哪儿,又是怎么被服务器认出来的。答案分两层,一层在客户端(Cookie),一层在服务端(Session),分别放在后面两章。这一章先把「无状态」本身说清楚,因为它是那两章的地基。
1.2 无状态的准确含义是服务器不保存客户端状态信息
无状态说的是:服务器不保存客户端的状态信息,每个请求都被当作全新的、独立的请求来处理,前后两个请求之间服务器不做任何关联。
- 请求必须自带全部信息。要访问哪个资源、要什么格式、是什么身份,全在这一条报文里,服务器不查「上次这个客户端说过什么」。
- 服务器不必为每个客户端留一块内存记会话,一个请求处理完就能把上下文全丢掉,内存占用不随在线人数线性增长。
- 请求可以落到任意一台机器上。既然服务器不记人,上一次打到 A 机、下一次打到 B 机,结果完全一样——加机器就能扛量,负载均衡随便轮询,这是无状态最大的工程价值。
- 代价是「认人」这件事协议不管了。业务需要知道「还是这个用户」,就得在 HTTP 之上自己补一层记忆,Cookie 与 Session 就是这层补丁。
1.3 「无连接」和「无状态」说的不是同一件事
教材里常见「HTTP 是无连接、无状态的协议」这句连读,很容易和长连接打架:既然无连接,keep-alive 又是在保什么?把两个词拆开就通了。
- 「无连接」讲的是 HTTP 交互的方式:一问一答就结束,HTTP 自己不维持「这个客户端和我之间有一条会话」这样的应用层上下文,也不负责连接的生命周期。连接是 TCP 的事——长连接保住的正是那条 TCP 连接,让它在多次 HTTP 交互之间不关。
- 「无状态」讲的是信息的归属:服务器不替客户端记住任何状态。哪怕 TCP 连接一直开着,服务器也不会因为「这条连接刚才发过一个登录请求」就默认后面还是同一个人。
- 两句话各管一个层面:无连接说的是「连接归 TCP 管」,无状态说的是「记忆归应用层自己管」。长连接加无状态可以同时成立,这也是 HTTP/1.1 的真实形态。
无连接是无状态的前提:正因为 HTTP 不假设连接上有上下文,服务器才敢把每个请求都当独立的;反过来,长连接并不改变无状态,它只是让底层那条 TCP 连接活得更久。
1.4 浏览器记住密码是浏览器的事,与 HTTP 无关
「以前登录过的网站,再打开时账号密码框自动带出来了」——这个现象常被当成「HTTP 其实记得我」的证据,其实不是。
- 那是浏览器把提交过的账号密码存在本地(密码管理器、自动填充),下次识别出同一个表单结构就填进去,数据始终在客户端。
- 判断依据很简单:换一台电脑、换一个浏览器,提示就没了;清掉浏览器数据,提示也没了。服务器那边什么都没做。
- 真正的登录态保持走另一条路:服务端在应答里下发凭证,浏览器保存并在后续请求里自动带上。这条路是后面两章的内容。
自动填充是客户端的本地便利功能,凭证往返是协议之上的会话机制;前者与服务器无关,后者才是「不用重新登录」的真正原因。
二、Cookie:把身份凭证交给浏览器保管
2.1 第一次登录,服务器用 Set-Cookie 把一段键值交给浏览器
登录是一次正常的 POST 请求,参数在正文里,服务器校验账号密码通过之后,要告诉客户端「记住这个身份」。做法是在响应报头里加一条Set-Cookie。
- 服务器构建应答,报头里带上
Set-Cookie这样一条键值对形式的凭证,然后把应答发回浏览器。 - 浏览器收到
Set-Cookie,按域名把这段键值保存下来——多数存内存,也有存本地文件的,区别在 2.3 讲。 - 保存之后浏览器还会记住它的作用域:属于哪个域名、哪个路径,决定了以后哪些请求该带上它。
真实报文里的Set-Cookie比「键=值」长,一条里还会跟着一串属性:Set-Cookie: sid=8f3a1c...; Path=/; Max-Age=3600; HttpOnly; Secure。一条Set-Cookie只能放一个键值对,要下发多个 cookie 就发多条。
2.2 之后的每个同域请求,浏览器自动带上 Cookie 头
这是 Cookie 最省事的地方:业务代码什么都不用干,浏览器负责把凭证带回去。
- 浏览器发请求前,按「域名 + 路径」匹配本地保存的 cookie,命中就把它们拼进
Cookie: sid=8f3a1c...这条请求报头。 - 服务器读
Cookie头取出凭证,反查这是哪个用户——「HTTP 无状态,却不用每次请求都验证」的答案就在这一步。 - 作用域是硬约束:
Domain决定哪些主机能收到(省略则只有签发它的主机自己能收到,最严格),Path决定哪些路径能收到。所以 A 站登录后拿到的 cookie 不会被带到 B 站,这是浏览器的同源隔离在起作用。
Cookie 的本质是「由服务器下发、由浏览器保管并在同域请求里自动回传的一段键值」,它解决的不是加密问题,而是「让无状态协议能认出同一个客户端」的问题。
2.3 会话 Cookie 随浏览器关闭消失,持久 Cookie 靠过期时间存活
同样是 cookie,关掉浏览器再打开,有的还在、有的没了,差别在有没有设过期时间。
- 不设
Expires也不设Max-Age,就是会话 Cookie:只存在内存里,浏览器进程结束就清掉,下次访问要重新登录。 - 设了
Expires(绝对时间)或Max-Age(存活秒数),就是持久 Cookie:写到本地存储,到期才失效,在这个窗口内重新打开浏览器仍然免登录。 - 两个都设时
Max-Age优先;Max-Age设 0 或负数等于立即失效,这是「退出登录」在协议层的标准做法之一。 - 一个例外值得知道:不少浏览器有会话恢复功能,崩溃或重启后会把上次的标签页连同会话 Cookie 一起恢复,看起来像持久化了,其实机制不同。
会话 Cookie 的生命周期绑在浏览器进程上,持久 Cookie 的生命周期绑在过期时间上;「关掉再打开要不要重新登录」这个体验差异,全在这一个属性上。
2.4 明文密码只是教学写法,真实凭证是服务端签发的一串随机值
把账号密码写成 cookie 值这个写法,把结构讲得很清楚——键值对、存在客户端、下次自动带回——但它不是真实站点的做法,真实 cookie 里不会出现账号密码。
- 真实下发的是服务端生成的随机串(会话 ID 或令牌),本身不含任何用户信息,读它、改它都推不出密码,只有服务端那张表能翻译出「这是谁」。
- Cookie 在客户端可读可改:本地文件能打开看,
document.cookie也能读能写。所以凭证不能携带敏感数据,只能是一个「指针」。 - 三个属性是标配,各堵一条路:
HttpOnly:JavaScript 读不到这条 cookie,挡 XSS 偷凭证;Secure:只在 HTTPS 上发送,挡明文链路被截;SameSite:限制跨站请求是否携带,挡 CSRF。
- 容量也有限:各浏览器普遍把单个 cookie 限制在 4KB 左右、单域名条数也有上限,这是实现限制不是协议规定——所以 cookie 只放标识,别放数据。
教学写法展示了「键值对存在客户端」这个结构,真实实现把值换成了不含信息的随机串,再用 HttpOnly、Secure、SameSite 三层属性把偷取、截获、冒用的路各堵一道。
三、Session:把敏感数据收回服务端
3.1 Cookie 文件被盗,等于身份被直接冒用
Cookie 存在客户端,客户端就是攻击者能够到的地方,所以它能被偷。
- 典型路径是恶意程序:木马落盘后扫描浏览器保存 cookie 的文件,把里面的凭证读走。
- 拿到凭证之后,攻击者在自己机器上手工构造请求,把
Cookie头填成偷来的值,发给同一个服务器。 - 服务器只认凭证不认人:来源 IP 变了、客户端换了,它都不会主动怀疑,直接按凭证对应的用户处理——这就是盗号。凭证在谁手里,谁就是那个用户,这是所有基于 cookie 的会话机制的共同软肋。
3.2 Session 用 session id 换掉敏感数据,应答里只回一个编号
如果 cookie 里装的是密码,被偷一次等于密码泄露;如果只装一个编号,被偷一次损失的只是这一份会话。Session 按后者设计。
- 用户登录成功后,服务端创建一份会话记录,里面存这个用户的敏感数据——用户 ID、昵称、权限、登录时间等,全部留在服务端。
- 服务端为这份记录生成唯一编号session id,用它作为键去匹配唯一用户;记录通常放内存或 Redis,可以设过期时间。
- 应答时只把 session id 下发到客户端,敏感数据一个字节都不出服务器。
- 此后浏览器每次请求自动带上这个 id,服务端拿 id 查表把会话数据取回来——客户端手里始终只有一个编号,改它也只能改成不存在的编号,推不出别人的信息。
Session 的实质是「数据留在服务端,客户端只拿一张提货单」:cookie 从「装着身份」退化成「指着一个身份」,被偷的损失从永久泄露压缩成一段会话。
3.3 Session 并未彻底解决安全,靠 IP 检测与会话失效收窄风险
Session 解决了「敏感数据不下发」这一层,没解决「凭证会被偷」这一层:session id 被偷,攻击者一样能冒充你,直到这个会话过期或失效。
- 所以传输层必须上 HTTPS——凭证在链路上是密文,中间人抓不到;再配
HttpOnly挡住脚本读取,把 XSS 偷凭证这条路也堵掉。 - IP 检测是一条常见辅助规则:同一份会话,请求来源突然换了个城市,服务端就要求重新验证。它的代价是会误伤——手机从 Wi-Fi 切到 4G、公司出口 IP 轮换都会触发重验。它是降低风险的手段,不是判断依据。
- 更直接的手段是让会话主动失效:用户点退出、修改密码、异地登录告警、后台检测到账号异常,服务端直接删掉这条会话记录,手里的旧 id 立刻变成一串查不到的死编号。
- 常说的「让 cookie-id 失效」,落地就是这一条——那个 id 指的是 session id,失效的对象是服务端那条会话记录,不是浏览器里那一小段字符串。
安全是一串接力:HTTPS 管链路、HttpOnly 管脚本、IP 检测管异常、会话失效管兜底;每一层只挡住一部分攻击,叠起来才能把风险压到可接受。
四、全篇总结
1. 无状态指服务器不保存客户端状态信息
- 每个请求自带全部信息,服务器处理完就丢;好处是省内存、可任意分机、易扩容,代价是「认人」得由应用层自己补。
2. 无连接说连接归 TCP 管,无状态说记忆归应用层管
- HTTP 自己不维护应用层会话上下文,长连接保住的是 TCP 连接,两者并不冲突,长连接加无状态才是 HTTP/1.1 的真实形态。
3. Cookie 是下发到浏览器的凭证,由浏览器自动回传
Set-Cookie下发、Cookie回传;会话 Cookie 随浏览器关闭消失,持久 Cookie 靠 Expires 或 Max-Age 存活;真实凭证是不含信息的随机串。
4. Session 把数据收回服务端,只给客户端一个 id
- 登录后服务端建会话、生成 session id,敏感数据留在服务端;id 被偷仍可冒充,所以要配 HTTPS、IP 检测与会话失效,层层收窄风险。
五、文末面试题
5.1 推导题
1. 无状态明明是限制,为什么反而被当成 HTTP 的优点?
答(推导):无状态牺牲的是「服务器自动记住你」,换来的是可伸缩性:服务器不必为每个在线用户保留会话内存,任何一台机器都能处理任何一个请求,加机器就能线性扛量,进程重启也不丢会话。反过来说,如果协议自带会话状态,负载均衡就得做粘性、节点故障就丢会话、扩容还要同步状态。业务需要的「记住人」被下沉到应用层,用 Cookie 加 Session 按需实现,协议本身保持轻——这就是这个设计的取舍。
2. Cookie 里为什么不能直接放用户信息,只能放一个随机凭证?
答(推导):因为 cookie 存在客户端,而客户端完全在用户的控制之下——本地文件能打开、document.cookie能读写。放明文用户信息等于把数据交出去,用户改一下就能伪造身份,攻击者读一次就直接拿到敏感数据。放随机凭证则把「数据」和「凭据」分开:客户端只有一个号码,内容在服务端,改号码只能改成无效号码。这条原则同样是 token 设计的出发点——令牌只做签名校验,不放可被解读的敏感字段。
3. Session 能不能彻底解决凭证被盗的问题?
答(推导):不能。Session 解决的是「敏感数据不再下发」,但 session id 本身仍然是一个可被偷走的凭据,偷到之后在有效期内一样能冒充。它把损失范围从「密码永久泄露」缩小到「一段会话」,这是收窄不是消除。要再往前推,需要在链路层加密(HTTPS)、在客户端限制读取(HttpOnly)、在行为上做异常检测(IP 变更、异地登录)、在机制上支持主动失效(退出、改密后清会话),这几层叠起来才把风险压到可接受。
5.2 真题
1. Cookie 的 HttpOnly、Secure 和 SameSite 分别有什么作用?
答(推导 · 已对照解析,转述):三个属性各堵一条攻击路径。HttpOnly 禁止 JavaScript 通过 document.cookie 读取这条 cookie,针对 XSS 窃取凭证;但它不阻止浏览器在脚本发起的请求里自动携带,所以降低的是「被读走」的风险,不是「被使用」的风险。Secure 限定这条 cookie 只在 HTTPS 连接上发送,防止明文链路被中间人抓走。SameSite 控制跨站请求是否携带 cookie,Strict 同站才带、Lax 允许顶层导航的 GET、None 必须同时设 Secure,它防的是 CSRF——让第三方站点伪造的请求没法自动带上凭证。
【真题·转述自 计算机网络面试题 - 小林coding】
2. 如果客户端禁用了 Cookie,Session 还能用吗?
答(推导 · 已对照解析,转述):还能用,但要换一种方式把 session id 带回去。默认机制是服务端通过 Set-Cookie 下发 id、浏览器自动回传;cookie 被禁用后浏览器不再保存和回传,就得让客户端每次请求手动携带,常见做法是 URL 重写——服务端把 session id 拼进页面里的每个链接,或者改用请求头、表单隐藏域传递。工程上更常见的是干脆不用服务端 Session,改用令牌方案,客户端把令牌放进 Authorization 头,不再依赖 cookie。代价是 URL 重写会把 id 暴露在地址栏和日志里,安全性反而更差。
【真题·转述自 计算机网络面试题 - 小林coding】
3. 携带 Cookie 的 HTTP 请求,算有状态还是无状态?
答(推导 · 已对照解析,转述):协议层仍然无状态,状态被搬到了报文里。服务器处理这次请求并不依赖「上次和这个客户端聊过什么」,依赖的是这条请求自己带的 Cookie——把这条请求原样发给另一台服务器,只要那台机器能查到同一份会话数据,处理结果一样。所以无状态说的是「服务器不靠内存上下文关联前后请求」,不是「请求里不能带状态信息」。真正的状态存在客户端的 cookie 和服务端的 session 表里,HTTP 只负责搬运。
【真题·转述自 计算机网络面试题 - 小林coding】
4. token、session、cookie 三者有什么区别?
答(推导 · 已对照解析,转述):cookie 是存储与传输的载体,属于客户端保存、按域自动回传的一段键值;session 是服务端保存的会话数据,客户端通常只拿一个 session id;token 是服务端签发的一串自带信息的凭证(如 JWT),验签即可确认身份,服务端不必存会话表。三者是层次不同而非互斥:session 和 token 是两种「记住身份」的方案,cookie 是它们最常见的搬运方式——session id 常放在 cookie 里,token 既可以放 cookie 也可以放请求头。选择看场景:单体应用用 session 简单可控,服务端能主动踢人;集群、跨端、无状态服务更适合 token,代价是无法即时吊销、体积更大。
【真题·转述自 计算机网络面试题 - 小林coding】
5. JWT 令牌为什么能解决集群部署下的会话问题?
答(推导 · 已对照解析,转述):因为校验身份所需的信息全在令牌本身,服务端不用存会话。传统 session 在集群下的麻烦是「状态归属」:用户第一次登录落在 A 节点、会话存在 A 的内存里,下一次请求被负载均衡打到 B 节点,B 查不到会话,于是要么做粘性会话把同一用户固定到 A,要么另建 Redis 集中存会话,两种都要额外机制。JWT 把用户标识和过期时间写进载荷、用服务端密钥签名,任何节点拿到令牌验签就能确认身份,节点之间不需要共享状态。代价是签发后无法即时吊销,只能靠短有效期加刷新令牌来兜。
【真题·转述自 计算机网络面试题 - 小林coding】
6. 把数据存在 localStorage 里,和存 Cookie 有什么区别?
答(推导 · 已对照解析,转述):最大的区别是「会不会自动发出去」。Cookie 由浏览器自动附加到同域请求上,服务端天然能收到,这正是它能承担会话凭证的原因;localStorage 只是浏览器本地存储,不发请求,要用得靠 JS 手动塞进请求头。其余差异跟着来:容量上 localStorage 约 5MB,Cookie 单个 4KB 左右;生命周期上 localStorage 不清就一直在,Cookie 可以设过期;安全上 localStorage 完全没有 HttpOnly 这样的保护,任何 XSS 都能直接读走,而 Cookie 至少能用 HttpOnly 挡住脚本。所以会话凭证优先放 HttpOnly Cookie,纯前端缓存的数据才放 localStorage。
【真题·转述自 计算机网络面试题 - 小林coding】
协议不记人,于是客户端存凭证、服务端存数据,一层层把风险收窄。评论区聊聊:你抓包时见过最离谱的一条 Cookie 里塞了什么?