Appearance
Cookie 相关知识梳理
难度等级:🟡 中级(2-4年)
分类:浏览器
说明:Cookie 本身概念不难,但涉及跨域、安全、Session 对比等,属于中级高频考点,也常作为浏览器/网络大题的切入口。
归档日期:2026-09-04
📚 理论知识
核心概念
Cookie 是服务器发送到浏览器并保存在本地的一小段数据(一般不超过 4KB),浏览器之后每次向**同源(严格说是同 domain+path 规则)**的服务器发起请求时,会自动把 Cookie 附加在请求头 Cookie 字段中带过去。它的本质作用是给无状态的 HTTP 协议提供"状态"——最常见的用途是身份认证(Session 标识)、个性化设置、用户行为追踪。
服务器通过响应头 Set-Cookie 来种下 Cookie:
Set-Cookie: sessionId=abc123; Max-Age=3600; Path=/; Domain=example.com; Secure; HttpOnly; SameSite=Lax关键属性详解
- Name=Value:键值对本体
- Domain:指定 Cookie 生效的域,不设置默认为当前域名(不含子域);设置后该域及其子域都可携带
- Path:指定生效路径,默认是设置该 Cookie 的路径
- Expires / Max-Age:过期时间。不设置则为 Session Cookie,浏览器关闭即失效;设置了就是持久 Cookie。
Max-Age优先级高于Expires(下文有专门展开) - Secure:只在 HTTPS 请求中才会被携带
- HttpOnly:禁止 JS 通过
document.cookie访问,主要用于防 XSS 窃取 - SameSite:控制跨站请求时是否携带 Cookie,是防 CSRF 的关键属性
Strict:完全禁止跨站携带Lax(现代浏览器默认值):导航到目标网站的 GET 请求(比如点链接)会带,但 POST 表单、iframe、图片等跨站请求不带None:允许跨站携带,但必须同时加Secure
工作流程
- 客户端首次请求,服务器在响应头里用
Set-Cookie种下 Cookie - 浏览器按规则(Domain/Path/Secure/SameSite)存储
- 之后凡是匹配规则的同域请求,浏览器自动在请求头带上
Cookie: name=value; name2=value2 - 服务器读取 Cookie 识别用户状态
Expires 与 Max-Age 的区别,过期后会自动删吗
- Expires 是一个绝对时间点,格式类似
Expires=Wed, 09 Jun 2027 10:18:14 GMT,是 HTTP/1.0 时代的老属性。它依赖客户端本地时间来判断,如果用户电脑时间不准,就可能出现该过期没过期、或者没过期就没了的情况。 - Max-Age 是一个相对时间,单位是秒,比如
Max-Age=3600表示"从现在起 3600 秒后过期",是 HTTP/1.1 引入的,不依赖本地系统时间的绝对值(虽然还是靠本地时钟计时,但不容易出现"服务器时间和客户端时间对不上"导致的绝对时间错乱问题)。 - 两者都设置时,Max-Age 优先级更高,浏览器会以它为准。
过期之后,浏览器会自动把这个 Cookie 从存储里清除,不需要任何人手动操作——下次访问该域名时,这个已过期的 Cookie 就不会再出现在请求头里了。这一点和"手动删除 Cookie"的原理其实是一回事:所谓"删除 Cookie",本质就是重新 Set-Cookie 一个同名同 Path 同 Domain 的 Cookie,把过期时间设成过去,浏览器发现过期了就清掉。
容易忽略的细节:Session Cookie(不设置 Expires/Max-Age 的)不是"永不过期",而是"浏览器关闭就没了",它和"过期删除"是两种不同的失效机制——一个是时间到了删,一个是会话结束(关浏览器)删,不完全等价于服务器 Session 的有效期。
跨站(Cross-Site)与跨域(Cross-Origin)的区别
这两个概念很容易混,但判断标准完全不同:
- 跨域(Cross-Origin):判断标准是协议 + 域名 + 端口三者是否完全一致,只要有一个不同就是跨域。这是浏览器同源策略(Same-Origin Policy)的判断依据,主要影响的是 JS 能不能通过 AJAX/Fetch 读取另一个源的数据,需要靠 CORS 来放开(参见 [[cors-same-origin]])。 例:
https://a.example.com:8080和https://b.example.com:8080—— 域名不同(子域不同),跨域。 - 跨站(Cross-Site):判断标准是有效顶级域名 + 二级域名(eTLD+1)是否一致,判断得比跨域"宽松"一些,这是 Cookie 的
SameSite属性在用的判断标准,主要影响的是要不要自动携带 Cookie。 例:还是https://a.example.com和https://b.example.com—— eTLD+1 都是example.com,属于同站(但跨域)。
关键结论:跨域 ≠ 跨站。两个子域名之间,通常是"跨域但同站"——AJAX 请求要过 CORS 这一关,但 Cookie 的 SameSite 判断上会被认为是"同站",所以哪怕 SameSite=Strict,子域名之间跳转 Cookie 依然会被携带。这也是为什么大公司做多个子系统单点登录时,经常会把 Cookie 的 Domain 设置成主域(比如 .example.com),让各个子域共享登录态。
一个更直观的例子:https://www.taobao.com 和 https://www.tmall.com 是完全不同的站点(跨站),哪怕域名看起来都是"淘宝系";而 https://www.taobao.com 和 https://login.taobao.com 是同站不同域。
对比辨析:Cookie vs Session vs LocalStorage vs Token(JWT)
| Cookie | Session | LocalStorage | Token/JWT | |
|---|---|---|---|---|
| 存储位置 | 浏览器 | 服务器(Cookie 存 SessionID) | 浏览器 | 浏览器(常存 LocalStorage/内存) |
| 容量 | ~4KB | 不限(服务器内存/DB) | ~5-10MB | 视存储方式 |
| 是否自动携带 | 是 | 依赖 Cookie 携带 SessionID | 否,需手动加到请求 | 否,需手动加到 Header |
| 跨域场景 | 需 SameSite/CORS 配置 | 同 Cookie | 天然不能跨域读取,但请求无自动携带问题 | 灵活,常用于前后端分离 |
| 是否易受 CSRF | 是(自动携带是根源) | 是 | 否 | 否(手动携带更安全) |
| 是否易受 XSS 窃取 | HttpOnly 可防 | 同 Cookie | 容易(JS 可读) | 若存 LocalStorage 也容易 |
Session 是什么,和 Cookie 是什么关系
Session 是一种在服务器端保存用户状态的机制,和 Cookie 是配合关系,不是并列的技术。
工作流程是这样的:
- 用户第一次登录成功后,服务器在内存(或 Redis、数据库)里创建一份数据,记录这个用户的登录状态、权限等信息,同时生成一个唯一标识——SessionID
- 服务器通过
Set-Cookie把这个 SessionID 种到浏览器里(Cookie 只是运输 SessionID 的载体,本身不存业务数据) - 之后用户每次请求,浏览器自动带上这个 Cookie(也就带上了 SessionID)
- 服务器拿到 SessionID,去自己的存储里查出对应的 Session 数据,从而知道"这是谁,登录了没有"
可以理解成:Cookie 存的是一把钥匙(SessionID),Session 存的是钥匙对应的房间里的东西(真正的用户数据)。这样设计的好处是敏感数据不会明文暴露在客户端;坏处是服务器要维护这份状态,如果是多台服务器负载均衡,就需要做 Session 共享(比如把 Session 统一存到 Redis 里,而不是存在某台机器的内存里),否则用户这次请求落到 A 机器、下次落到 B 机器,会出现"登录状态丢失"的问题。这也是现在很多分布式系统更倾向用无状态的 Token/JWT 方案的原因之一——JWT 把用户信息直接编码在 Token 里(加签名防篡改),服务器不需要存储任何 Session 数据,天然免去了这个共享难题(更完整的 JWT 方案参见 [[jwt-token]])。
Session 的完整生命周期:Cookie 什么时候会被重新种下
默认情况下,"种 Cookie"这个动作分工很清晰:
- 登录接口:验证用户名密码通过后,服务器创建 Session(存到内存/Redis),生成 SessionID,通过
Set-Cookie种到浏览器里。这是唯一需要"种 Cookie"的地方。 - 其他所有接口(比如获取用户信息、下单、查列表):只需要读请求头里带过来的 Cookie,取出 SessionID,去 Session 存储里查一下"这个 SessionID 对应的用户是谁、登录了没有、有没有过期",然后正常处理业务逻辑,不需要重新
Set-Cookie。
也就是说,"种 Cookie"这个动作在整个用户会话周期里通常只发生一次(登录那一刻),后面 N 次请求都是纯读取校验,不会重复种。
但实际项目里,还有几个场景也会触发重新 Set-Cookie:
- 续期/滑动过期(Rolling Session):如果希望"用户只要一直在操作,就不掉线",服务器会在业务接口里顺带判断"这个用户还活跃",然后重新
Set-Cookie刷新Max-Age,这样每个接口理论上都可能重新种一次。很多框架(比如 Express 的express-session配rolling: true)是自动做这件事的,业务代码不用手动处理。 - 退出登录接口:也需要种 Cookie——不过是种一个已过期的同名 Cookie,让浏览器把本地那份删掉,同时服务器端也要把对应的 Session 数据销毁掉(否则光删客户端 Cookie 没用,如果 SessionID 泄露了还是能被人拿去伪造请求)。
- 换取新 Token/刷新令牌:如果用的是 JWT 双 Token 机制(accessToken + refreshToken),刷新接口也会重新种 Cookie 下发新的 token。
更完整的说法是:"种 Cookie"这个动作只发生在"身份状态发生变化"的接口(登录、登出、续期、刷新令牌),而绝大多数业务接口只做"读取校验",不参与种/删 Cookie 这个动作。这也是权责分离比较清晰的设计——业务逻辑接口不应该关心身份认证细节,这部分逻辑通常会抽到一层中间件(middleware)里统一处理,业务代码从中间件处理完的 req.user 或类似对象里直接拿当前用户信息就行。
关键细节 / 容易忽略的点
- Cookie 是当前域名下所有请求自动携带的,不区分是不是 AJAX 发起的,这也是 CSRF 攻击的根源
- 浏览器对单个域名的 Cookie 数量(一般 50 个左右)和总大小有限制,超出会被丢弃或覆盖旧的
- 子域名设置
Domain=example.com后,a.example.com和b.example.com都能访问,但反过来子域名种的 Cookie 默认不会被父域访问 - 前后端分离 + 跨域场景下,想让 Cookie 跨域携带,前端要
fetch(url, {credentials: 'include'})或axios.defaults.withCredentials = true,后端要Access-Control-Allow-Credentials: true且Access-Control-Allow-Origin不能写*,必须是具体域名 document.cookie读写是字符串拼接式操作,写入一次只能加一个 Cookie,且无法直接删除,只能通过设置过期时间为过去来删除
常见误区
- 认为 Cookie 和 Session 是并列的两种技术——实际上 Session 依赖 Cookie(或 URL 重写)来传递 SessionID,二者是配合关系
- 认为设置了 HttpOnly 就完全安全——它只防 XSS 窃取 Cookie,防不住 CSRF,也防不住其他类型的 XSS 攻击(比如页面篡改)
- 认为
SameSite=Lax能完全防 CSRF——Lax 下 GET 类型的跨站导航请求仍会带 Cookie,如果后端把敏感操作做成 GET,一样可能被打 - 认为跨域就是跨站——两个子域名之间通常是"跨域但同站",AJAX 要过 CORS 这一关,但 SameSite 判断上会被认为是同站,Cookie 依然会被携带
❌ 错误答法示范
错误示例 1:「Cookie 和 LocalStorage 差不多,都是浏览器存东西的地方,就是大小不一样。」
❌ 问题:完全忽略了两者最本质的区别——Cookie 会自动随请求发送到服务器,而 LocalStorage 不会。这个区别直接决定了它们的应用场景(Cookie 适合服务器需要感知的身份状态,LocalStorage 适合纯前端本地存储)以及各自的安全风险(Cookie 关联 CSRF,LocalStorage 关联 XSS 数据泄露)。
错误示例 2:「设置了 HttpOnly,Cookie 就不会被黑客拿到了,很安全。」
❌ 问题:HttpOnly 只是防止 JS 脚本读取 Cookie(防御 XSS 窃取手段之一),但完全没有涉及 CSRF——攻击者不需要读取 Cookie 内容,只需要诱导用户浏览器发起请求,Cookie 依然会被自动带上。安全是要多层防护配合(HttpOnly + SameSite + CSRF Token + Secure)的。
🎤 模拟面试作答
嗯,这道题我来说一下我的理解。
Cookie 本质上是为了解决 HTTP 无状态这个问题而生的。因为 HTTP 请求本身是没有记忆的,服务器不知道这次请求和上次请求是不是同一个用户发的,所以就需要一个东西来"记住"用户状态,Cookie 就是干这个的。
流程上是这样的:服务器第一次响应的时候,通过 Set-Cookie 这个响应头把数据种到浏览器里,浏览器会根据 Domain、Path、过期时间这些规则把它存起来,之后凡是匹配这些规则的请求,浏览器都会自动在请求头里把 Cookie 带上,不需要我们手动处理,这也是它和 LocalStorage 最大的区别——LocalStorage 是纯前端存储,不会自动跟请求走。
Cookie 上有几个比较关键的属性我印象比较深:一个是 HttpOnly,设置了之后 JS 就没法通过 document.cookie 读到这个值了,主要是防 XSS 攻击窃取 Cookie;一个是 Secure,代表只在 HTTPS 下才发送;还有一个是 SameSite,这个是控制跨站请求要不要带 Cookie 的,现在浏览器默认是 Lax,就是说像点链接这种跳转会带,但是跨站的 POST 表单、iframe 这些是不带的,这个属性其实是防 CSRF 攻击的关键一环。这里有个细节,SameSite 判断的是"跨站"不是"跨域",跨站的标准是有效顶级域名加二级域名是否一致,比 CORS 判断跨域的标准(协议+域名+端口)要宽松,所以两个子域名之间哪怕 AJAX 要过 CORS,Cookie 在 SameSite 判断上依然算同站,会正常携带,这也是很多系统做多子域名单点登录的基础。
过期时间上,Expires 是绝对时间点,依赖客户端本地时钟;Max-Age 是相对秒数,两者都设置的话 Max-Age 优先级更高。过期之后浏览器会自动清掉,不需要手动处理,而所谓"手动删除 Cookie",本质上也是重新种一个同名同路径的 Cookie,把过期时间设成过去。
Cookie 和 Session 是配合关系不是并列关系——Session 是服务器端存状态的机制,Cookie 只是运输 SessionID 这把"钥匙"的载体。登录成功后服务器创建 Session、生成 SessionID,种进 Cookie;之后的业务接口大多数时候只是读取校验这个 SessionID,不会重复种 Cookie,只有登录、登出、续期、刷新令牌这类"身份状态发生变化"的接口才会重新 Set-Cookie。
另外我们项目里之前做前后端分离,遇到过跨域请求 Cookie 带不过去的问题,后来发现是两边都要配置:前端请求要加 withCredentials: true,后端的 CORS 响应头要加 Access-Control-Allow-Credentials: true,并且 Access-Control-Allow-Origin 不能写通配符 *,必须写明确的域名,这两个条件缺一个都不行。
如果面试官感兴趣,我还可以聊聊 Cookie、Session、Token 三者的对比,或者展开讲一下 CSRF 具体是怎么利用 Cookie 自动携带这个特性来攻击的。
❓ 面试官追问预测
追问 1:Cookie、Session、Token(JWT)到底怎么选?
应对思路:从"状态存哪"和"是否自动携带"两个维度对比。Session 依赖服务器存状态、扩容需要做会话共享;Token 是无状态的,天然适合分布式和前后端分离,但要自己处理存储和携带(防 XSS 风险),面试中可以结合项目场景说明选型理由。
追问 2:CSRF 攻击的原理是什么,怎么防御?
应对思路:核心是"浏览器自动携带 Cookie"这个机制被利用——攻击者诱导用户在已登录状态下访问恶意页面,恶意页面向目标网站发起请求,Cookie 自动带上从而伪造身份。防御手段:SameSite 属性、CSRF Token、验证 Referer/Origin。
追问 3:SameSite 的三个值具体区别,Lax 是默认值吗?
应对思路:Strict 完全不带、Lax 部分场景带(如 GET 导航)、None 都带但强制要 Secure。现代主流浏览器(Chrome 80+)把 Lax 设为默认值,这是一个重要的浏览器行为变化,可以提一下这对老项目跨域登录可能造成的兼容性问题。
追问 4:Cookie 有大小和数量限制吗?超了会怎样?
应对思路:单个 Cookie 一般不超过 4KB,单域名 Cookie 数量浏览器普遍限制在 50 个左右(不同浏览器略有差异),超出限制新写入的可能覆盖旧的或直接被丢弃,实际开发中不该把 Cookie 当大容量存储用。
追问 5:如何在前端读写删除 Cookie?有没有用过 js-cookie 之类的库?
应对思路:原生是通过 document.cookie 字符串拼接读写,删除是设置过期时间为过去时间点;手写解析比较繁琐(要处理分号分隔、encode/decode),实际项目中更常用 js-cookie 这类库来简化操作,可以提一下自己是否封装过相关工具函数。
追问 6:为什么两个子域名之间 AJAX 请求需要 CORS,但 Cookie 却能正常携带?这不矛盾吗?
应对思路:不矛盾,因为"能不能读响应"(CORS/同源策略管的事)和"要不要携带 Cookie"(SameSite 管的事)是两套完全独立的判断标准,用的还是不同的宽松程度——同源策略按协议+域名+端口精确判断跨域,SameSite 按 eTLD+1 判断跨站,两个子域名之间是"跨域但同站",所以 Cookie 会被携带,但响应内容还是要服务端配置 CORS 响应头才能被 JS 读到,两件事各走各的规则,不能因为 Cookie 带过去了就以为跨域限制解除了。
追问 7:如果服务器用负载均衡部署了多台机器,Session 方案要怎么解决"这次请求落到 A 机器、下次落到 B 机器导致登录状态丢失"的问题?
应对思路:常见方案有几种——一是 Session 共享,把 Session 数据统一存到 Redis 这类外部存储,所有机器共享同一份数据源,不管请求落到哪台机器都能查到;二是负载均衡层做会话粘滞(Session Sticky),保证同一个用户的请求始终转发到同一台机器,但这样牺牲了负载均衡的均匀性,机器故障时该用户的 Session 也会丢;三是干脆放弃有状态的 Session,改用无状态的 JWT 方案,把用户信息编码进 Token 本身,任何一台机器拿到 Token 验证签名就能独立完成鉴权,不需要查任何共享存储。
补充:
- JavaScript 删除 Cookie 是通过 document.cookie = "...; Max-Age=0" 或 Expires✅ HttpOnly Cookie 无法通过 JavaScript 操作,只能由服务器删除。
- Cookie 和 LocalStorage 是 Web 标准里浏览器实现的能力,App 和小程序作为独立的宿主环境,各自实现了功能对等但 API 完全不同的替代方案——本地存储层面它们都有(只是不叫这两个名字),但"请求自动携带身份标识"这个 Cookie 独有的便利性,在这些环境里基本都退化成了手动挂 Header 的 Token 方案,安全性上反而因为"显式可控"变得更好把控,只是需要开发者自己多做一步存取和挂载的工作。 这个问题面试官很喜欢追问"那你怎么保证 Token 安全存储",可以顺着上面这段展开。IOS好像有个叫 keychain 的东西,安卓也有类似的