Cookie (~ 4 MB)
Cookie 是服务器发送到用户浏览器并保存在本地的一小段文本数据,浏览器在后续请求中会自动携带同源的 Cookie,用于会话管理 、个性化设置 和跟踪分析。
示例 设置 cookie
http://localhost:5177/react19-vite-app/bug-view
js
<button onClick={() => {
document.cookie = "theme=dark";
// 读取 cookie
console.log(document.cookie)
}}>
设置 Cookie
</button>

服务器通过响应头 Set-Cookie: 设置
js
Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
存活时间
Max-Age,秒数(相对时间),从当前请求开始,Cookie 有效的秒数。例如Max-Age=3600表示 1 小时后过期。Expires, HTTP-date 绝对时间(若与 Max-age 同时存在,Max-Age 优先), 指定 Cookie 过期的具体时间点(UTC)。例如Expires=Wed, 21 Oct 2026 07:28:00 GMT。- 未设置
Expires或Max-Age时,Cookie 在浏览器关闭时删除(会话结束)。
浏览器会自动管理 Cookie 的生命周期。一旦 Cookie 的过期时间到达(通过 Expires 或 Max-Age 指定),浏览器会立即将其从 Cookie 存储中删除或标记为无效。
建议存储的数据类型
- 用户登录后,服务器生成一个唯一的
sessionId存入 Cookie,后续请求自动携带,服务器据此识别用户会话状态。 - 用户偏好设置。例如语言选择、主题(暗色/亮色)、字体大小、页面布局模式等。
不建议存储的数据类型
- 密码(明文或哈希都不可)
- 支付令牌、信用卡号
- 个人身份信息(身份证、手机号等)
- 大体积数据(>4KB)
- 敏感业务逻辑数据
请求如何携带 cookie
示例 HTTP 同源 (@tinyhttp/app)
同源请求时,浏览器的 fetch 会自动携带 Cookie,不需要额外设置。

可以看到 cookie只传递了两个

示例 HTTP 跨域带上 cookies
js
<button onClick={() => {
fetch('http://localhost:3000/api/cookies-1',{
credentials: "include", // 发送 cookie
}).then(res => {
console.log('res',res)
})
}}>
跨域请求
</button>

当请求带有 credentials: "include" 配置,后端 设置 access-control-allow-credentials为true。Access-Control-Allow-Origin 必须是 具体域名
js
app.get("/api/cookies-1", (req, res) => {
res.setHeader("access-control-allow-credentials", "true");
res.setHeader("access-control-allow-origin", "http://localhost:5177");
res.send("hello cookies");
});

示例 XMLHttpRequest 同源
同源请求:默认会自动带上 Cookie。

示例 XMLHttpRequest 跨域
跨域请求 默认不会自动带上 Cookie,需要手动设置 withCredentials = true。
js
const ajaxFetch = (url: string, withCredentials?: boolean) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log("xhr.responseText", xhr);
}
};
if (withCredentials) {
xhr.withCredentials = true;
}
xhr.send();
};

示例 HTTPS (express)
前端项目 react19 + vite 8
1、同源。fetch 默认带上 cookie
vite 配置代理
js
proxy: {
"/eapi": {
target: "https://localhost:8443", // https 项目 (express)
changeOrigin: true,
secure: false, // 跳过证书
rewrite: (path) => {
return path.replace(/^\/eapi/, "/api")
},
},
},

2、跨域
当 fetch 使用 credentials: "include" 时,服务器返回头部 Access-Control-Allow-Origin 必须是 具体域名,否则会报跨域错误。
js
app.get('/api/user', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:5177');
res.header('Access-Control-Allow-Credentials', 'true');
const userId = req.query.id;
if (userId) {
const user = users.find(u => u.id === parseInt(userId));
if (user) {
res.json(user);
} else {
res.status(404).json({ error: 'User not found' });
}
} else {
res.json(users);
}
});
示例 HTTP/2
页面 https://localhost:5177/react19-vite-app/bug-view
1、同源

2、跨域请求发送
example1 : 跨域
js
<button onClick={() => {
fetch('https://localhost:8843/api/user',{
}).then(res => {
console.log('res',res)
})
}}>
跨域请求 Cookie
</button>

example2: 跨域请求带上 cookie
js
<button onClick={() => {
fetch('https://localhost:8843/api/user',{
credentials: "include", // 发送 cookie
}).then(res => {
console.log('res',res)
})
}}>
跨域请求 Cookie
</button>

解决措施?access-control-allow-origin, 必须指定具体 域名路径,不能是 *。当跨域请求带上 凭据信息,需要设置 access-control-allow-credentials 为true。
js
const getUser = (stream, headers) => {
stream.respond({
"access-control-allow-origin": "https://localhost:5177",
"access-control-allow-credentials": true,
"content-type": "application/json; charset=utf-8",
":status": 200,
});
const user = {
id: Date.now(), // 使用时间戳作为唯一ID
name: "lili",
age: 20,
address: "Beijing",
email: "hyan@example.com",
createTime: new Date().toISOString(),
};
stream.end(JSON.stringify(user));
};

cookie 属性
1、 Secure 标记。确保 Cookie 仅通过 HTTPS 传输,避免在 HTTP 连接中被窃听或篡改。
2、SameSite 属性, 用于控制 Cookie 是否在跨站请求中发送 ,主要目的是防御 CSRF(跨站请求伪造)攻击。
Strict。Cookie 仅在同站请求中发送。任何来自其他站点的请求(包括顶级导航,如点击链接跳转)都不会携带该 Cookie。Lax (默认值,现代浏览器)同站请求发送;部分跨站请求允许发送,具体包括:- 顶级导航的 GET 请求(如点击
<a>链接) - 表单 GET 提交。不允许:POST 表单提交、iframe、AJAX、img 等跨站请求。
- 顶级导航的 GET 请求(如点击
None。所有跨站请求都携带 Cookie。必须同时设置 Secure 属性(即 Cookie 仅通过 HTTPS 传输),否则浏览器会拒绝。
3、 HttpOnly 标记。用于防止客户端脚本(如 JavaScript)通过 document.cookie 访问该 Cookie。

只能拿到一个cookie, 因为 lastVisit 设置了 httpOnly 为true。
js
<button
onClick={() => {
console.log("cookie", document.cookie); // theme=light
}}
>
cookie
</button>
存在风险 XSS
XSS(跨站脚本攻击)是一种注入攻击,攻击者将恶意脚本(通常是 JavaScript)注入到目标网站中。当其他用户访问该网站时,恶意脚本在受害者浏览器中执行。如果 Cookie 未设置 HttpOnly 属性,则可以通过 document.cookie 被 JavaScript 读取,从而导致 Cookie 泄露。
攻击者操作流程?
- 注入恶意脚本:攻击者通过评论、表单输入或 URL 参数等方式,向目标网站注入一段恶意代码。
- 用户访问恶意页面:当受害者访问包含该脚本的页面时,浏览器执行该脚本。
- Cookie 被窃取 :脚本读取
document.cookie(前提是 Cookie 未设置HttpOnly),获取到用户的身份凭证(如 sessionId),然后发送到攻击者的服务器。 - 攻击者利用 Cookie:攻击者拿到 Cookie 后,可将其添加到自己的请求头中,假冒受害者访问网站,执行敏感操作。
为什么 Cookie 容易成为 XSS 的目标?
- 自动携带:Cookie 通常用于身份认证,在每次同源请求中自动发送。一旦泄露,攻击者可直接冒充用户。
- 存储敏感信息:许多网站将 session ID 或 token 存储在 Cookie 中,这些凭证正是攻击者想要窃取的目标。
- 默认可被脚本读取 :若未显式设置
HttpOnly,所有同源 JavaScript 都可以通过document.cookie读取 Cookie 内容。
Cookie 的 HttpOnly 如何防御 XSS?
设置了 HttpOnly 属性的 Cookie,浏览器会禁止 JavaScript 通过 document.cookie 访问。
存在风险 CSRF
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式,攻击者诱导用户访问恶意网站,该网站利用用户浏览器自动携带目标网站的 Cookie 这一特性,伪造用户发起的请求,执行非预期操作。
攻击者步骤?
- 用户登录信任网站
A(如银行网站),浏览器保存了A的认证 Cookie。 - 用户在不退出
A的情况下,访问攻击者控制的恶意网站B。 - 网站
B中嵌入一个隐藏请求(如img标签、iframe、表单自动提交等),指向A的敏感接口(如https://bank.com/transfer?to=attacker&amount=10000)。 - 浏览器发起该请求时,会自动携带
A的 Cookie,服务器验证通过,执行敏感操作。
攻击者不需要窃取 Cookie,只需要"借用"浏览器自动携带的能力即可。
Cookie 为何易受 CSRF 攻击?
默认同源策略 :Cookie 的自动附加机制是浏览器内置行为,无法通过前端代码禁止(除非设置 SameSite 限制跨站发送)。
防御 CSRF 的措施?
SameSite,Cookie 属性, 限制跨站请求是否携带 Cookie。SameSite=Lax或Strict可阻止大部分 CSRF。CSRF Token, 服务端生成随机 token 嵌入页面,每个请求需携带该 token(通常放在请求头或参数中)。- 验证
Referer/Origin头 检查请求来源是否为信任域名。 - 要求自定义请求头, 如 X-Requested-With: XMLHttpRequest,因为跨站请求无法添加自定义头(需 CORS 预检)。
sessionStorage (5-10MB)
sessionStorage 是 Web Storage API 提供的一种客户端存储机制,用于在单个浏览器标签页(或窗口)的会话期间存储键值对数据。它提供了一种比 Cookie 更简单、容量更大的临时存储方案。
数据仅在当前标签页或窗口的会话期间有效。关闭标签页或浏览器后,数据被清除。
同源 (协议、域名、端口相同)且同一标签页 。不同标签页之间不共享(即使同源)。
只能存储字符串
js
// 存储数据
sessionStorage.setItem('key', 'value');
sessionStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));
// 读取数据
const value = sessionStorage.getItem('key');
建议存储的数据类型
- 用户界面状态, 折叠/展开的侧边栏状态、当前激活的 Tab 索引、列表滚动位置、模态框是否已显示过
- 表单临时草稿, 多步骤表单中已填写的数据(用户未最终提交)
- 临时缓存数据, 从 API 获取的、但仅在当前标签页使用的数据
不适合存储的数据
- 认证凭证(JWT、Access Token), 任何同源脚本(包括 XSS 攻击脚本)都能直接读取,无法像 HttpOnly Cookie 那样防止 JavaScript 访问。
- 用户敏感信息, 如身份证号、手机号、家庭住址、支付密码等。
- 长期有效的 API 密钥, 一旦被 XSS 窃取,攻击者可以长期滥用。
- 个人隐私数据, 任何可用于识别用户身份或进行欺诈的数据。
- 需要跨标签页共享的数据, sessionStorage 在不同标签页之间隔离,不适合存储需要多个标签页同时访问的信息
易受 XSS 攻击
sessionStorage 是浏览器提供的 Web 存储 API,用于在同源 且同一标签页 的会话期间存储键值对数据。与 Cookie 不同,sessionStorage 不支持 HttpOnly 属性 ,因此任何同源的 JavaScript 脚本都可以通过 sessionStorage.getItem() 或直接访问 sessionStorage 对象来读取其中的数据。这使得 sessionStorage 成为 XSS 攻击的窃取目标。
攻击者步骤?
- 注入恶意脚本:攻击者通过网站漏洞(如未过滤的评论、URL 参数、输入框)注入 JavaScript 代码。
- 读取 sessionStorage :恶意脚本执行
sessionStorage.getItem('token')或遍历sessionStorage获取所有存储的数据。 - 发送到攻击者服务器:窃取的数据(例如认证令牌、用户信息)被发送到攻击者控制的服务器。
- 利用窃取的数据:攻击者可冒充用户发起请求,或获取敏感信息。
localStorage (5-10MB)
localStorage 是 Web Storage API 提供的客户端存储机制,用于在浏览器中持久保存 键值对数据。与 sessionStorage 相比,localStorage 的数据没有过期时间,除非主动删除,否则会一直存在。
同源 (协议、域名、端口相同)的所有标签页/窗口共享。
js
// 存储数据
localStorage.setItem('key', 'value');
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));
// 读取数据
const value = localStorage.getItem('key');
// 删除单个数据
localStorage.removeItem('key');
// 清除所有数据
localStorage.clear();
当同一源的其他标签页/窗口修改 localStorage 时,当前页面会触发 storage 事件。
注意:该事件仅在不同标签页中修改时触发,在修改数据的同一标签页不触发。
建议存储数据类型
- 用户偏好设置:主题(暗色/亮色)、字体大小、语言选择等,永久保存。
- 登录状态(前端) :存储 JWT token(非敏感场合),每次请求手动添加到
Authorization头。 - 缓存数据:如 API 响应结果,减少网络请求(需注意数据新鲜度)。
- 购物车数据(非支付场景):存储商品列表,关闭浏览器后依然存在。
- 用户行为记录:如教程完成度、首次访问标记等。
存在 XSS 攻击
localStorage 与 sessionStorage 类似,都受到 同源策略 保护,且 没有类似 Cookie 的 HttpOnly 属性 。因此,只要网站存在 XSS(跨站脚本)漏洞,恶意脚本就可以直接读取 localStorage 中的所有数据,导致敏感信息泄露。
攻击者过程?
- 注入恶意脚本:攻击者通过评论区、搜索框、URL 参数等注入 JavaScript 代码。
- 读取 localStorage :恶意脚本执行
localStorage.getItem('key')或遍历localStorage获取全部存储内容。 - 发送数据到攻击者服务器:窃取的数据被发送到攻击者控制的服务器。
- 利用窃取的数据:攻击者可冒充用户(如使用窃取的 JWT token)、获取用户隐私等。
Cache API
Cache API 是一种浏览器提供的持久化请求/响应缓存机制 ,主要用于缓存网络请求和对应的响应对象 。它与 localStorage、IndexedDB 不同,专门设计用于存储 HTTP 请求响应(Request 和 Response 对象),是构建离线 Web 应用(PWA)和优化网络性能的核心工具。
Cache API 只能在安全上下文中使用(HTTPS 或 localhost)。
响应对象不可重复使用 :Response 是一个流,只能读取一次。存储前需要克隆:response.clone()。
indexedDB
IndexedDB 是一个运行在浏览器中的非关系型(NoSQL)数据库,用于在客户端存储大量结构化数据。
回顾 fetch API
js
fetch(url,options)
js
interface RequestInit {
method?: string
keepalive?: boolean
headers?: HeadersInit
// arrayBuffer arrayBufferView blob formData USVString ReadableStream URLSearchParams
body?: BodyInit | null
// 重定向处理 'error' | 'follow' | 'manual'
redirect?: RequestRedirect
integrity?: string
// 请求信号 用于取消请求的信号
signal?: AbortSignal | null
// "omit" | "include" | "same-origin"
credentials?: RequestCredentials
// 请求模式 'cors' | 'navigate' | 'no-cors' | 'same-origin'
mode?: RequestMode
referrer?: string
// 控制Referer头部的策略
referrerPolicy?: ReferrerPolicy
window?: null
dispatcher?: Dispatcher
duplex?: RequestDuplex
}