程云来 / 杭州
返回文章·计算机网络
·约 15 分钟

网络面试(十七):HTTP 无状态,登录状态放在哪里?

追踪登录后的会话标识,区分 Cookie、Session、Token 与 JWT,再用跨域提交订单解释 CORS、预检、CSRF 和 XSS 的边界。


第二次请求,服务器怎样认出你?

用户 U42 登录成功,随后查看订单 O204。两个 HTTP 请求都经过 TCP 和 HTTP 解析,但协议不会自动告诉订单接口“这是刚才登录的那个人”。

应用必须建立关联:登录时确认身份,发放凭据;后续请求携带凭据,服务器验证,再检查用户是否有权访问 O204。识别登录用户与允许访问某个订单,是两次不同判断。

本章用设定的会话与报文字段解释,不提供可部署的认证服务。凭据 demo-S17 只是易读标签,真实会话标识必须按安全要求随机生成,不能采用这种可猜的字符串。

先看同源登录:页面和接口都在 https://shop.example.com。服务端校验登录信息后,创建会话记录,例如把随机标识关联到 U42、有效期与必要状态,再通过响应字段发给浏览器:

http
Set-Cookie: __Host-sid=demo-S17; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800

这里展示的是字段,不是完整响应。浏览器是否保存,还受 Cookie 接受规则和用户设置影响;并不是服务器写了字段就保证保存成功。随后符合条件的请求会携带:

http
Cookie: __Host-sid=demo-S17

请求 Cookie 中没有把 Secure、HttpOnly、Max-Age 等属性原样送回去。服务器收到标识,查询对应会话,确认未过期、未撤销,再检查 O204 是否属于 U42。RFC 6265:Cookie 的存储与发送

名称解决的问题本例在哪里
Cookie浏览器怎样保存并按规则附带一小段数据浏览器保存 sid,请求带 Cookie 字段
Session(会话)应用怎样维护多次请求之间的用户状态服务端保存 sid 与 U42 的关联
Token(令牌)请求凭什么被认证或获得某种权限本例 sid 就是一种不透明凭据
JWT怎样用标准格式表达声明并保护其完整性等属性可作为另一种令牌格式,不是本例必需项

Cookie 可以装会话标识,也可以装某种令牌;Token 可以放 Cookie,也可以由客户端放进 Authorization。服务端会话通常存在内存或共享存储中,不要求每次请求都落到最初那台机器。多个实例如何读取一致状态、如何处理故障,是应用部署问题。

HTTP 无状态不意味着“服务器没有数据”,也不意味着“不能复用连接”。业务会话、TCP 连接、HTTP 缓存分别有自己的身份与寿命,不能因为都能跨请求使用,就把它们当成同一件事。

属性或形式主要约束不能由此推出
未设置 Domain通常形成仅当前主机的 host-only Cookie自动发给所有兄弟子域
Domain=example.com在合法作用域内可覆盖该域及其子域能任意为别人的域设置 Cookie
Path=/orders按路径匹配决定发送为同站点脚本建立安全隔离墙
Secure在安全传输条件下发送Cookie 内容已在浏览器本地加密
HttpOnly阻止通过 document.cookie 等脚本接口直接读取恶意脚本不能发带 Cookie 的请求
SameSite根据同站/跨站上下文限制发送等同于同源策略或全部 CSRF 防护
Max-Age / Expires控制浏览器中 Cookie 的保留时间服务端会话一定在同一时刻失效

Max-Age 与 Expires 都出现时,Max-Age 优先。没有二者的会话 Cookie,也不能承诺“关浏览器就永远消失”:会话恢复等浏览器行为会影响观察结果。

现代浏览器支持的 __Host- 前缀要求 Secure、Path=/,且不能设置 Domain,可收紧主机作用域;它仍不替服务端做认证、权限校验或 XSS 防护。新属性与前缀的浏览器接受规则要按目标实现核验,不能只依赖早期 RFC 6265 的正文。Mozilla 官方浏览器文档:Set-Cookie 属性与前缀

SameSite 常见值有 Strict、Lax、None。Strict 对跨站发送限制更强;Lax 仍允许某些跨站顶层安全导航,不能理解为“所有跨站请求都禁用”;None 允许跨站使用,但现代浏览器要求与 Secure 一起设置,且仍受第三方 Cookie 等策略限制。未显式设置的默认行为还可能有实现细节,因此应明确选择策略。Chrome 团队:SameSite Cookie 解释

用户退出时,通常既要让服务端会话失效,也要删除对应 Cookie。删除须匹配名称、作用域与路径等条件;只删除浏览器标识,不能让已经泄露的凭据在服务端自动失效。登录成功或权限变化后还应更新会话标识,避免把攻击者预先控制的旧会话继续当成登录凭据。OWASP:会话生命周期与标识更新

JWT 为什么不等于“无服务器状态且绝对安全”?

JWT 可以携带用户、签发者、受众与到期时间等声明。常见的签名 JWT 能验证内容是否被修改,但编码后的载荷并不因此保密;能解码看到声明,不代表能伪造有效签名。加密形式另有机制,不能把所有 JWT 都概括成“加密字符串”。RFC 7519:JWT 格式

服务端不能只解码就认定登录有效。它要按自己的信任配置验证签名、允许的算法、签发者、受众和适用时间等条件;不能让不可信令牌随意决定验证方式。RFC 8725:JWT 最佳实践

自包含令牌可以减少每次查询会话的需要,但立即退出、权限变更、撤销与刷新仍需设计。短有效期、撤销记录等方案有不同成本,不存在“用了 JWT 就彻底不要任何状态”的普遍结论。

保存位置也不是银弹:放在脚本可读存储里,要考虑 XSS 读取;放在 HttpOnly Cookie 里,要考虑自动附带凭据的请求与 CSRF。还要考虑凭据寿命、泄露后撤销和服务端授权,不能只按 Cookie 与 Token 的名字选安全等级。

跨域与跨站,是同一个判断吗?

浏览器同源策略中的 origin(源),通常由协议、主机、端口构成。与 https://shop.example.com 比较:

地址是否同源原因
https://shop.example.com/orders是路径变化不改变源
https://shop.example.com:443/profile是HTTPS 默认端口就是 443
https://api.example.com/orders否主机不同
https://shop.example.com:8443/orders否端口不同
http://shop.example.com/orders否协议不同

SameSite 使用的是“站”的上下文:现代判断考虑协议与可注册域等因素,不按端口区分,也不是按最后两个域名字符串机械截取。例如两个 HTTPS 子域 shop.example.com 与 api.example.com 不同源,但通常同站。公共后缀、嵌套页面和请求上下文也会影响站点判断。HTML 标准:源与同站判断

所以“跨域请求一定不带 Lax Cookie”不成立;跨源可能仍是同站。Cookie 的主机/路径匹配、SameSite、Fetch 凭据模式和浏览器政策,都要分别满足。

CORS 允许谁读取哪份响应?

下面改用一个前后端分开部署的场景:页面在 https://shop.example.com,接口在 https://api.example.com。假设用户已通过合法登录获得 api 主机自己的 Cookie;前面的 shop host-only Cookie 不会自动搬过去。

页面向 API 发 JSON 创建订单,并携带 CSRF 请求标识。Fetch 使用 credentials: include 允许携带符合规则的 API 凭据,但这不能绕过 Cookie 作用域、SameSite 或浏览器阻止政策。

跨源 JSON POST 和自定义 X-CSRF-Token 字段不符合免预检的条件。假设没有可用的预检缓存,浏览器先发送:

http
OPTIONS /orders HTTP/1.1
Host: api.example.com
Origin: https://shop.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-csrf-token

预检询问的是浏览器跨源权限,不是用户已经通过了业务认证。按 Fetch 标准,跨源预检本身不带凭据,所以不要要求 OPTIONS 必须先带登录 Cookie 才能得到正确预检响应。

API 校验允许的源、方法与字段后,可以返回以下相关字段:

http
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type, x-csrf-token
Vary: Origin

成功预检还需使用合适的成功状态,例如 204。浏览器随后发实际 POST;API 仍需验证会话、CSRF 标识及订单操作权限,实际响应也要有匹配的 Allow-Origin 与 Allow-Credentials,才能让页面脚本读取。

正在绘制图表…
查看 Mermaid 源码
sequenceDiagram
    participant P as shop 页面脚本
    participant B as 浏览器
    participant S as api 订单服务
    P->>B: 跨源 JSON POST,允许携带凭据
    B->>S: OPTIONS,声明源/方法/字段,无 Cookie
    S-->>B: 204 + 允许的源/方法/字段/凭据模式
    B->>S: 实际 POST + API Cookie + CSRF 标识
    Note over S: 验证会话、CSRF 与订单权限
    S-->>B: 业务响应 + 匹配的 CORS 字段
    B-->>P: 通过 CORS 检查后暴露响应

Mermaid 大图

可滚动查看图表,点击缩放比例可恢复 100%。按 Esc 关闭。

带 credentials: include 的响应不能用 Access-Control-Allow-Origin: * 代替明确允许的源。动态返回源时应先按允许列表检查,再考虑 Vary: Origin;不能无条件回显任意 Origin。Vary 解决缓存区分,不是认证机制。WHATWG Fetch:CORS 与凭据

并非所有跨源请求都会预检:符合方法、字段等限制的请求可能直接发出。即使脚本最后因 CORS 看不到响应,服务器仍可能已经处理了这类请求。因此 CORS 报错不能证明业务没有执行,CORS 也不是接口的身份验证。 curl 等非浏览器客户端不会替服务器执行浏览器的读响应限制。WHATWG Fetch:CORS 协议

预检结果有专门的缓存,Access-Control-Max-Age 控制其有效期并受浏览器上限影响;它与上一章保存业务正文的 HTTP 缓存不同。mode: no-cors 也不是解决接口跨域的通用开关:它会限制请求,并使相应响应对脚本不透明,不能拿来读取订单 JSON。

动手区分预检、业务执行与响应读取

先走全部通过,再比较预检拒绝与实际响应 CORS 不匹配。浏览器都可能报错,但服务端创建效果相同吗?最后看看业务授权拒绝时 CORS 能做什么。

浏览器报跨域错误,订单一定没创建吗?

页面在https://shop.example.com,接口在https://api.example.com。两个HTTPS子域通常同站但不同源;API使用自己的凭据。

服务端证据

实际 POST:未发送
订单效果:尚未创建

页面脚本证据

尚未取得业务响应

1 / 4

固定教学分支,不发真实请求、不运行认证或订单服务。假设API Cookie被允许发送、CSRF标识有效,业务授权单独切换;跨源预检无凭据,实际带凭据响应不能使用*允许源。免预检请求另有路径,CORS不是防止所有请求发送或接口身份认证的机制。

CSRF 与 XSS,各自借用了什么?

CSRF(跨站请求伪造)利用的是浏览器在条件满足时会自动附带目标站点凭据。用户登录着订单站点,另一个站点诱导浏览器提交操作;攻击者未必需要读取响应,就可能造成业务效果。

服务端应让敏感操作需要可验证的请求来源与意图。常见组合包括与会话关联的 CSRF Token、严格核验 Origin/Referer、合适的 SameSite、禁止用 GET 改状态,以及按部署采用 Fetch Metadata 等防护。CSRF Token 不是登录会话标识,不应把 sid 原样拿来替代;实现需要生成、传递、验证和失败拒绝路径。SameSite 适合作为多层防护的一部分,不能单独证明所有请求安全。OWASP:CSRF 防护

XSS(跨站脚本)则让不可信内容在站点的脚本上下文中执行。即便 HttpOnly 阻止它直接读 sid,脚本仍可能发起合法作用域内的请求,或读取页面数据。CSRF Token 也可能被同源恶意脚本取得或绕过使用场景。

防 XSS 需要按输出上下文编码、使用安全的 DOM API,富文本还需合适的净化;CSP 等策略可以增加保护,但不替代正确处理不可信内容。OWASP:XSS 防护

机制主要保护范围本例仍要做的事
CORS浏览器向跨源脚本共享响应的条件认证、授权与请求来源防护
HttpOnly限制脚本直接读取 Cookie防止 XSS 代用户发请求
SameSite限制某些跨站 Cookie 发送处理允许的场景与同站攻击风险
CSRF Token验证特定请求具有预期的会话关联防 XSS、校验业务权限

不要从“有一个保护字段”推出整条操作链安全。应逐步回答:凭据是否有效、请求是否来自允许场景、用户是否有订单权限、不可信内容能否执行。

30—60 秒参考回答

HTTP 不会自动维护业务登录会话,应用通过凭据关联请求。Cookie 是浏览器保存和附带数据的机制,Session 是应用维护的会话状态,Token 是凭据,JWT 是一种令牌格式,它们可以组合。Cookie 属性分别限制发送、脚本读取和寿命,不能替代认证授权。跨源按协议、主机、端口判断,跨站则有另一套规则。CORS 控制浏览器向跨源脚本共享响应,预检不等于认证,也不能防止所有请求发送。CSRF 借自动携带凭据制造操作,XSS 在站点上下文执行代码,需要分别防护。

四个追问及解析

1. HttpOnly 是否能防住 XSS?

它能限制脚本直接读取该 Cookie,但不能阻止恶意脚本运行,也不能阻止脚本利用浏览器已有会话请求接口。要修复不可信内容的执行路径,并验证业务权限,不能只添加 HttpOnly 就结束处理。

不一定,两者由不同组件管理。浏览器停止附带 Cookie,不会自动删除服务端记录;服务端撤销会话,也不要求浏览器先删除 Cookie。退出、超时和撤销应设计成明确的服务端与客户端流程。

3. CORS 失败,重试创建订单会不会重复?

可能。如果实际请求已经发送并处理,只是响应未通过浏览器共享检查,业务结果仍可能成功。结合网络面板与服务端证据判断,并按第 14 章的请求键与结果查询契约处理,不能把 CORS 错误当作安全重试证明。

4. JWT 验签成功,可以直接访问任意订单吗?

不可以。还需验证签发者、受众、有效期等适用条件,确认当前账户和权限,再对具体订单做授权。凭据有效只解决一部分身份问题,不会把所有资源权限自动授予持有者。

两个易错辨析

“前端设置 Access-Control-Allow-Origin,就能解除跨域。” 这个许可来自服务器响应。页面无法靠自己加同名请求字段让浏览器授权读取,服务端必须按允许的源和凭据模式配置。

“用了 Authorization Token,就永远没有 CSRF。” 显式附带令牌与自动 Cookie 发送有不同风险,但令牌也可能装在 Cookie,实际架构还可能使用其他自动凭据。按凭据如何发送判断,并继续防 XSS、泄露和越权,不能只看 Token 这个名字。

下一章解释 HTTPS 如何保护传输,以及证书、主机名验证和密钥协商怎样确认正在与谁通信。