目录

网络-14 Cookie、Session 与跨域

前置阅读:网络-12 HTTP 报文、方法与状态码

HTTP 是无状态的——服务端不会记住上一个请求是谁发的。但真实业务需要"登录状态"这种概念,于是有了 Cookie 机制。而 Cookie 天生和"域"绑定,这又牵出了同源策略和跨域问题。这三个话题是连着的,所以放在一篇里讲。

1. Cookie

1.1 基本机制

① 服务端在响应里下发:
   Set-Cookie: session_id=abc123; Path=/; HttpOnly

② 浏览器保存,之后每次请求该域名自动带上:
   Cookie: session_id=abc123

关键点:浏览器自动携带。这是 Cookie 最大的便利,也是 CSRF 攻击的根源——后面会讲。

1.2 属性详解

Set-Cookie: id=abc123; Domain=example.com; Path=/; Max-Age=3600;
            Secure; HttpOnly; SameSite=Lax
属性 作用
Domain 生效域名。设为 example.com子域名也会带上api.example.com 生效);不设则只对当前精确域名生效
Path 生效路径,/ 表示全站
Expires 绝对过期时间
Max-Age 相对过期秒数,优先级高于 Expires
Secure 只在 HTTPS 下发送
HttpOnly 禁止 JS 通过 document.cookie 读取
SameSite 控制跨站请求是否携带,防 CSRF

会话 Cookie vs 持久 Cookie:不设 Expires/Max-Age 的叫会话 Cookie,浏览器关闭即失效(存内存);设了的叫持久 Cookie,写入磁盘。

HttpOnly 是防 XSS 的关键:如果攻击者往你页面注入了脚本,没有 HttpOnly 时一行 document.cookie 就能偷走会话凭证。存放 session_id 的 Cookie 必须HttpOnly

SameSite 的三个取值

行为
Strict 完全不允许跨站携带。最安全,但从外部链接跳进来会显示未登录
Lax 允许顶级导航的 GET 请求携带(点链接跳转),阻止 POST 和 iframe/img 等子资源请求。现代浏览器的默认值
None 允许所有跨站携带,但必须同时设置 Secure

Lax 成为默认值是近年浏览器的重大变化——它默认阻止了跨站 POST,从根本上削弱了传统 CSRF 攻击。需要跨站携带 Cookie(如独立部署的前后端)必须显式设 SameSite=None; Secure

注意 SameSite 判断的是"站点(Site)“而非"源(Origin)”a.example.comb.example.com同站但不同源。站点比对的是 eTLD+1(可注册域名),比同源策略宽松。

1.3 局限

  • 容量小:每个域名下通常 4KB 左右、条数有限;
  • 每个请求都带:即使请求图片、CSS 也会带上,浪费带宽(所以静态资源常放在独立域名下,即"Cookie-free domain");
  • 只能存字符串

2. Session、Token 与 JWT

2.1 Session(服务端存储)

① 用户登录成功
② 服务端生成 session_id,把用户信息存在服务端(内存/Redis)
③ Set-Cookie: session_id=abc123 下发给浏览器
④ 后续请求带上 session_id,服务端据此查出用户信息

问题在分布式场景:服务端要存状态,多台机器就要解决共享问题。三种解法:

  • 粘性会话(Sticky Session):负载均衡按 IP 哈希,同一用户固定打到同一台。缺点是那台机器挂了会话就丢,且扩缩容时会话大面积失效。
  • Session 复制:机器之间互相同步。机器多了同步开销爆炸。
  • 集中存储:统一存 Redis。这是现在的主流做法,代价是多一次网络查询和一个中心依赖。

2.2 Token(客户端存储)

① 用户登录成功
② 服务端签发一个 token(自带用户信息 + 签名)
③ 客户端自己保存(localStorage / sessionStorage)
④ 后续请求手动放到 Authorization: Bearer <token>

服务端不存状态,只验证签名,天然适合分布式和多端(App、小程序没有 Cookie 概念)。

2.3 JWT 的结构

JWT(JSON Web Token)是 Token 的一种具体实现,由三段 Base64URL 编码的内容用 . 连接:

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxIiwiZXhwIjoxNzAwfQ . SflKxwRJSMeKKF2QT4
└──── Header ────────┘ └──────── Payload ──────────┘ └─── Signature ───┘
      算法声明              用户信息+过期时间              签名

最关键的认知:Payload 只是 Base64 编码,不是加密。任何人都能解开看到内容。 所以 JWT 里绝对不能放密码、身份证号等敏感信息。签名的作用是防篡改(保证完整性),不是防偷看(不保证机密性)。

2.4 三者对比

Session Token / JWT
状态存储 服务端 客户端
分布式扩展 需 Redis 等共享存储 天然支持
能否主动失效 ✓ 删掉即可 ✗ 签发后无法撤销
传输方式 Cookie 自动携带 手动放 Header
CSRF 风险 (自动携带) (手动携带)
XSS 风险 可用 HttpOnly 防护 存 localStorage 无法用 HttpOnly 防护
多端支持 依赖 Cookie,App 端不便

JWT 最大的软肋是无法主动失效。用户改密码、管理员封号、检测到异常登录,签出去的 token 在过期前依然有效。常见缓解手段:

  1. 缩短有效期 + Refresh Token:access token 设几分钟到几小时,配一个长效 refresh token 用于续期;
  2. 维护黑名单:把要撤销的 token ID 存 Redis——但这就重新引入了服务端状态,等于放弃了 JWT 的核心优势。

所以选型上:单纯的 Web 应用用 Session + Redis 往往更简单可靠;需要多端、跨服务、无状态鉴权时 JWT 更合适。别因为 JWT “更现代"就无脑选它。

3. 同源策略

同源 = 协议 + 域名 + 端口 三者完全相同

https://www.example.com:443/a 为基准:

URL 是否同源 原因
https://www.example.com:443/b 仅路径不同
http://www.example.com 协议不同
https://api.example.com 子域名也算不同源
https://www.example.com:8443 端口不同

同源策略是浏览器的核心安全机制。没有它,你打开一个恶意网页,它的 JS 就能直接读取你银行网站的数据。

受限制的:XMLHttpRequest / fetch、DOM 访问、读取 Cookie 和 localStorage。

不受限制的(历史遗留,也是 CSRF 的基础):

  • <img src><script src><link href><iframe src> —— 可以跨域加载
  • <form> 提交 —— 可以跨域提交

注意一个关键细节:跨域请求其实是发出去了的,服务端也处理了,只是浏览器拦截了响应不让 JS 读取。这解释了一个常见困惑:“为什么后端日志显示收到请求了,前端却报跨域错误”——因为拦截发生在响应返回后。

也正因为如此,同源策略只约束浏览器。服务端之间互相调用、curl、Postman 都不存在跨域问题。

4. CORS 跨域资源共享

CORS 是 W3C 的标准解决方案,核心是服务端通过响应头明确授权。它把请求分成两类。

4.1 简单请求

同时满足以下条件才算简单请求:

  • 方法是 GET / HEAD / POST 之一;
  • Content-Type 只能是 text/plainmultipart/form-dataapplication/x-www-form-urlencoded 三者之一;
  • 没有自定义请求头。

简单请求直接发出,浏览器自动加上 Origin 头:

GET /api/data HTTP/1.1
Origin: https://www.example.com

服务端同意就在响应里带上:

Access-Control-Allow-Origin: https://www.example.com

浏览器检查这个头,匹配则放行,不匹配就拦截响应并在控制台报错。

4.2 预检请求(Preflight)

不满足简单请求条件的(比如发 Content-Type: application/json 的 POST,或带自定义 header 的请求),浏览器会先自动发一个 OPTIONS 请求试探:

OPTIONS /api/data HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

服务端回应允许的范围:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400        ← 预检结果缓存 24 小时

预检通过后才发真正的请求。

为什么需要预检? 因为简单请求的那个白名单,恰好覆盖了 HTML 表单本来就能跨域提交的范围——这些能力在 CORS 出现前就存在,所以不需要额外许可。而 PUTDELETE 或自定义头是表单做不到的新增能力,必须先征得服务端同意,避免旧服务端在毫不知情的情况下被跨域调用而产生副作用。

Access-Control-Max-Age 很重要:不设的话每个非简单请求都要多一次 RTT。设了之后浏览器缓存预检结果,同样的请求在有效期内直接发。

4.3 携带凭证

跨域请求默认不发送 Cookie。需要携带时两边都要配合:

// 前端
fetch(url, { credentials: 'include' })
# 服务端
Access-Control-Allow-Origin: https://www.example.com    ← 不能是 *
Access-Control-Allow-Credentials: true

这里有个硬性限制Allow-Credentials: true 时,Allow-Origin 不能是通配符 *,必须是明确的源。这是刻意的安全设计——否则任意网站都能带着用户 Cookie 访问你的接口。

实践中常见的做法是:从请求的 Origin 头读取来源,与白名单比对,匹配则原样回显。注意不要无脑回显 Origin(等价于放行所有来源),必须校验白名单。

4.4 Nginx 配置

location /api/ {
    # 预检请求直接在网关层返回,不打到后端
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin  $http_origin;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Control-Max-Age 86400;
        return 204;
    }

    add_header Access-Control-Allow-Origin  $http_origin always;
    add_header Access-Control-Allow-Credentials true always;
    proxy_pass http://backend;
}

生产环境应把 $http_origin 换成白名单校验(用 map 指令),而不是直接回显。

4.5 其他跨域方式

方式 说明
反向代理 让前端和 API 处于同源,Nginx 转发到后端。生产环境最常用,因为根本不产生跨域
JSONP 利用 <script> 不受限制的特性。只支持 GET,已过时
postMessage 页面与 iframe/新窗口之间通信
WebSocket 不受同源策略限制(见 网络-19),但服务端应校验 Origin

5. CSRF

CSRF(Cross-Site Request Forgery,跨站请求伪造)利用的正是 Cookie 自动携带这个特性。

攻击流程:

① 用户登录了 bank.com,浏览器持有其 Cookie
② 用户在同一浏览器打开了恶意页面 evil.com
③ evil.com 中埋了:
   <img src="https://bank.com/transfer?to=hacker&amount=10000">
   或一个自动提交的表单
④ 浏览器发出这个请求时,自动带上了 bank.com 的 Cookie
⑤ 服务端看到合法 Cookie,执行了转账

注意攻击者全程看不到响应(被同源策略拦住了),但副作用已经产生。这也说明为什么"不安全方法必须用 POST 而不是 GET"很重要——见 网络-12 里那个把删除写成 GET 的反面案例。

防御手段(应组合使用):

手段 原理
SameSite=Lax/Strict 从根本上阻止跨站携带 Cookie。现代浏览器默认 Lax,这是最有效的一层
CSRF Token 服务端下发随机 token,放在表单隐藏域或自定义请求头。攻击者因同源策略读不到这个 token
校验 Origin / Referer 判断请求来源是否合法。注意两者都可能缺失(隐私设置),不能作为唯一手段
敏感操作二次验证 转账、改密码要求输入密码或短信验证码
不用 GET 做写操作 <img><link> 这类标签无法触发副作用

为什么 Token 认证天然抗 CSRF? 因为 token 存在 localStorage 里,需要 JS 手动放进 Authorization 头。攻击者的页面因同源策略读不到你的 localStorage,也没法让浏览器自动带上——不存在"自动携带"这个前提,攻击链就断了。

代价是 localStorage 无法用 HttpOnly 保护,一旦有 XSS 漏洞,token 就会被直接读走。所以:

  • Session + Cookie:怕 CSRF,但可用 HttpOnly 抗 XSS;
  • Token + localStorage:天然抗 CSRF,但对 XSS 毫无防护。

两种方案是风险互换,不存在哪个绝对更安全。折中做法是把 token 也放进 HttpOnly Cookie,再配 SameSite 和 CSRF Token——同时拿到两边的防护。

6. 本章面试题

Cookie 和 Session 的区别?
  • Cookie浏览器端的存储机制,由 Set-Cookie 下发,之后每次请求自动携带。容量约 4KB,只能存字符串。
  • Session服务端的会话状态,通常把 session_id 通过 Cookie 传给浏览器,服务端据此查出用户信息。

关系是:Session 的实现依赖 Cookie 传递标识。Session 数据在服务端(内存/Redis),Cookie 只存一个 id。

Session 和 JWT 怎么选?JWT 的最大缺点是什么?

JWT 最大缺点:签发后无法主动失效。 用户改密码、管理员封号,token 在过期前依然有效。缓解手段是缩短有效期 + Refresh Token,或维护黑名单(但黑名单重新引入服务端状态,等于放弃了 JWT 的核心优势)。

选型:单纯 Web 应用用 Session + Redis 往往更简单可靠;需要多端(App/小程序)、跨服务、无状态鉴权时选 JWT。别因为"更现代"无脑选 JWT。

补充:JWT 的 Payload 只是 Base64 编码,不是加密,任何人可解开。绝不能放敏感信息,签名只防篡改不防偷看。

什么是同源策略?跨域请求到底发出去了吗?

同源 = 协议 + 域名 + 端口三者完全相同。子域名不同也算跨域。

请求确实发出去了,服务端也处理了,只是浏览器拦截了响应不让 JS 读取。这就是"后端日志有记录但前端报跨域"的原因。

同源策略只约束浏览器——服务端互调、curl、Postman 都不受限。

不受限制的例外:<img><script><link><iframe> 可跨域加载,<form> 可跨域提交——这也是 CSRF 的基础。

CORS 的预检请求是什么?为什么需要它?

不满足简单请求条件时(非 GET/HEAD/POST、Content-Type: application/json、带自定义头),浏览器先自动发一个 OPTIONS 请求询问服务端是否允许。

为什么需要:简单请求的白名单恰好是 HTML 表单本来就能跨域做到的事,无需额外许可。而 PUT/DELETE 或自定义头是新增能力,必须先征得服务端同意,避免旧服务端在不知情的情况下被跨域调用产生副作用。

Access-Control-Max-Age 缓存预检结果,否则每个非简单请求都多一次 RTT。

跨域携带 Cookie 要注意什么?

前端设 credentials: 'include',服务端设 Access-Control-Allow-Credentials: true

硬性限制:此时 Access-Control-Allow-Origin 不能是 *,必须是明确的源。这是刻意的安全设计,否则任意网站都能带用户 Cookie 访问接口。

实践中从 Origin 头读取并与白名单比对后回显,不要无脑回显(等于放行所有来源)。

CSRF 的原理和防御?为什么 Token 认证天然抗 CSRF?

原理:利用 Cookie 自动携带的特性。用户登录 bank.com 后访问恶意页面,页面里的 <img src="bank.com/transfer?..."> 会让浏览器自动带上 Cookie 完成攻击。攻击者看不到响应,但副作用已产生。

防御(组合使用)SameSite=Lax/Strict(最有效,现代浏览器默认)、CSRF Token(攻击者因同源策略读不到)、校验 Origin/Referer、敏感操作二次验证、不用 GET 做写操作。

Token 抗 CSRF 的原因:token 需要 JS 手动放进 Authorization 头,攻击者读不到你的 localStorage,也无法让浏览器自动携带——“自动携带"这个前提不存在,攻击链就断了。

代价:localStorage 无法用 HttpOnly 保护,XSS 下 token 直接被读走。两种方案是风险互换

HttpOnly 和 SameSite 分别防什么?
  • HttpOnlyXSS:禁止 JS 通过 document.cookie 读取。存 session_id 的 Cookie 必须加。
  • SameSiteCSRF:控制跨站请求是否携带 Cookie。Strict 完全禁止,Lax(默认)只允许顶级导航的 GET,None 全允许但必须配 Secure

注意 SameSite 判断的是**站点(eTLD+1)**而非源,a.example.comb.example.com 是同站不同源。