标签

Cookie

Cookie(也称为网页cookie、浏览器cookie)是由网站创建的小型文本文件,当用户浏览该网站时,它被存储在用户的设备上。Cookie 的主要作用是帮助网站记住关于您的访问信息,比如登录状态、用户偏好设置、购物车中的物品等,从而在您再次访问网站时能够提供更个性化的用户体验。

Cookie
服务端5月30日 01:00
什么是 Cookie?它的工作原理和用途是什么?Cookie 是浏览器保存的一小段键值数据,通常用来让 HTTP 这种“无状态协议”记住用户状态。流程很简单:服务端在响应里用 `Set-Cookie` 下发 Cookie,浏览器按 Domain、Path、Expires、SameSite 等规则保存;之后访问匹配的地址时,浏览器会自动把 Cookie 放进 `Cookie` 请求头发回服务端。它常用于登录会话、用户偏好、购物车和统计分析。敏感会话 Cookie 一般要配 `Secure`、`HttpOnly`、`SameSite`,否则容易被窃取或被跨站请求滥用。 ## 追问 ### Cookie 和 localStorage 有什么区别? Cookie 会随匹配请求自动发送给服务器,适合会话识别;localStorage 只在浏览器本地保存,不会自动进请求头,适合非敏感前端数据。 ### Session Cookie 和持久 Cookie 有什么区别? 不设置 `Expires` 或 `Max-Age` 的通常是会话 Cookie,浏览器会话结束后失效;设置了过期时间的是持久 Cookie。 ### Cookie 为什么有安全风险? 因为它可能代表登录态。被 XSS 读走会导致会话劫持,被 CSRF 利用会让浏览器自动带着 Cookie 发起恶意请求。 ### SameSite、HttpOnly、Secure 分别解决什么问题? `SameSite` 缓解 CSRF,`HttpOnly` 防止脚本读取,`Secure` 限制只走 HTTPS。 ## 写段代码 ```http Set-Cookie: sessionId=abc; Path=/; Secure; HttpOnly; SameSite=Lax Cookie: sessionId=abc ```
服务端5月30日 01:00
如何在 JavaScript 中设置、读取和删除 Cookie?JavaScript 操作 Cookie 主要靠 `document.cookie`。读取时它返回当前页面可访问的 Cookie 字符串;设置时一次只能写入一个 `name=value`,不会清空其他 Cookie;删除本质是把同名 Cookie 的过期时间设为过去。实际开发要注意两点:值要用 `encodeURIComponent` 编码,删除时 `Path`、`Domain` 要和设置时一致,否则看起来执行了,其实没删掉。`HttpOnly` Cookie 不能被 JavaScript 读取或设置,只能由服务端通过 `Set-Cookie` 下发。 ## 追问 ### document.cookie = xxx 会覆盖所有 Cookie 吗? 不会。它只会新增或更新当前这一个 Cookie。读取 `document.cookie` 才会看到多个 Cookie 拼成的字符串。 ### 为什么删除 Cookie 有时不生效? 大概率是 `Path` 或 `Domain` 不一致。比如设置时用了 `Path=/app`,删除时只写 `Path=/`,浏览器会认为它们不是同一个 Cookie。 ### 为什么要编码 Cookie 值? Cookie 值里如果有空格、分号、中文等特殊字符,可能导致解析错误。写入用 `encodeURIComponent`,读取用 `decodeURIComponent`。 ### JavaScript 能设置 HttpOnly 吗? 不能。`HttpOnly` 的目的就是禁止脚本访问 Cookie,必须由服务端设置。 ## 写段代码 ```javascript function setCookie(name, value, maxAge, path = '/') { document.cookie = `${name}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=${path}`; } function getCookie(name) { return document.cookie.split('; ').find(v => v.startsWith(name + '=')) ?.split('=').slice(1).join('=') || null; } function deleteCookie(name, path = '/') { document.cookie = `${name}=; Max-Age=0; Path=${path}`; } ```
服务端5月30日 01:00
Cookie 浏览器兼容有哪些差异?如何处理?Cookie 浏览器兼容差异主要集中在容量限制、SameSite 默认值、第三方 Cookie、隐私保护策略和过期时间。现在不能再假设“所有浏览器都会长期保存并发送 Cookie”:Safari 的 ITP、Firefox 的 ETP、Chrome 的第三方 Cookie 收紧都会影响跨站登录、埋点和广告归因。处理思路是默认一方 Cookie、SameSite=Lax、Secure 开启,跨站场景单独设计降级方案。 ## 追问 ### 各浏览器容量差异要怎么记? 单个 Cookie 通常按 4KB 估算,每个域名数量上限各家不同。面试里不必背精确数字,关键是不要依赖“能存很多”,超过少量会话字段就该换存储方案。 ### SameSite=None 为什么必须配 Secure? 因为 `SameSite=None` 表示允许跨站发送 Cookie,风险更高。现代浏览器要求它只能在 HTTPS 下配合 `Secure` 使用,否则可能直接拒收。 ### Safari ITP 对业务影响最大在哪里? 跨站追踪、第三方登录态和长期归因最容易出问题。即使代码没变,Cookie 也可能被限制有效期或阻止发送,所以要用真实 Safari 和隐私模式测试。 ### 第三方 Cookie 被禁后怎么办? 优先改成一方登录态;确实跨域时,用 OAuth 跳转、服务端会话、postMessage 或浏览器提供的新 API,而不是继续依赖隐藏 iframe 写 Cookie。 ## 写段代码 ```javascript document.cookie = 'sid=abc; Path=/; Secure; SameSite=Lax'; // 跨站且必须携带时: document.cookie = 'sso=abc; Path=/; Secure; SameSite=None'; ```
服务端5月30日 01:00
Cookie 的 Domain 和 Path 有什么作用?如何设置?Cookie 的 Domain 决定哪些域名会收到它,Path 决定哪些路径会收到它。默认不写 Domain 时,Cookie 通常只属于当前 host;写成父域如 `example.com` 时,子域也可能共享。Path 是路径前缀匹配,不是权限隔离,主要用于减少无关请求携带 Cookie。正确做法是按最小范围设置,登录态、后台、支付、静态资源不要混用一个宽泛 Cookie。 ## 追问 ### 不写 Domain 和写 Domain 有什么区别? 不写 Domain 更收敛,通常只给当前主机使用。写父域会扩大到子域,适合 SSO,但也扩大了泄露和冲突范围。现在前导点 `.example.com` 基本被忽略,关键是域本身是否合法。 ### Path=/api 能防止前端 JS 读取 Cookie 吗? 不能。Path 控制请求是否携带 Cookie,不是安全边界。要防 JS 读取,应设置 `HttpOnly`;要跨站防护,还要配合 `SameSite` 和 CSRF 方案。 ### Path 的匹配规则是什么? 它是前缀匹配。`Path=/admin` 会匹配 `/admin` 和 `/admin/user`,但设计时最好写清楚边界,避免 `/admin-old` 这类路径造成误判。 ### 实际项目怎么设置? 会话 Cookie 尽量只给需要鉴权的域和路径;静态资源放到无 Cookie 域名;管理后台、支付、开放 API 分开命名和作用域,排查时也更清楚。 ## 写段代码 ```http Set-Cookie: sid=abc; Domain=example.com; Path=/; HttpOnly; Secure; SameSite=Lax Set-Cookie: admin=xyz; Path=/admin; HttpOnly; Secure; SameSite=Strict ```
服务端5月30日 01:00
Cookie 性能如何优化?怎样减少大小和数量?Cookie 性能优化的核心是少带、少存、少匹配。因为同域请求会自动携带匹配 Cookie,Cookie 越多,请求头越大,移动网络和高并发接口越容易被拖慢。实战里优先把会话凭证保留在 Cookie,把偏好、缓存、草稿这类非敏感数据迁到 localStorage、sessionStorage 或 IndexedDB;同时收窄 Domain、Path,避免静态资源和无关接口也背着 Cookie 跑。 ## 追问 ### Cookie 为什么会影响接口性能? 每次 HTTP 请求都会带上匹配的 Cookie。即使只是请求一张图片,只要域名和路径匹配,也可能多传几 KB 请求头。HTTP/2 有 HPACK 压缩,但不能把设计不当的 Cookie 变成零成本。 ### 减少 Cookie 大小有哪些做法? 只存必要字段,键名和值都尽量短;多个小偏好可以合并成一个值,但不要把大对象硬塞进去。超过 4KB 或频繁变化的数据不适合放 Cookie。 ### Domain 和 Path 怎么配合性能优化? Domain 不要随手设成 `.example.com`,Path 也不要一律 `/`。例如后台令牌只给 `admin.example.com` 和 `/admin`,API Cookie 只给 `/api`,静态资源域名最好不设置 Cookie。 ### 项目里常见坑是什么? 分析、AB 实验、灰度标记容易越积越多,最后每个请求都多带一串历史垃圾。上线前可以在网关或浏览器 DevTools 里看 Request Headers,超过 2KB 就该清理。 ## 写段代码 ```javascript function cookieBytes() { return new Blob([document.cookie]).size; } if (cookieBytes() > 2048) { console.warn('Cookie too large:', cookieBytes()); } ```
服务端5月29日 01:22
Cookie 和 Session 有什么区别?何时用 Cookie,何时用 Session?Cookie 存在客户端浏览器,每次请求自动携带,上限约 4KB,可被用户查看和篡改;Session 存在服务端(内存/Redis/数据库),客户端只持有 Session ID(通常通过 Cookie 传递),数据大小无硬性限制,安全性更高。核心区别是状态存储位置:Cookie 是客户端状态,Session 是服务端状态。选择依据:非敏感偏好数据(主题、语言)用 Cookie;登录态、权限等敏感数据用 Session。Session 的局限在于服务端需存储状态,分布式场景需用 Redis 等集中式存储共享 Session,否则粘性Session限制了水平扩展。JWT 是第三条路:将状态编码进 Token 本身,服务端无状态验证签名即可,但 Token 体积大且无法主动失效。 ## 追问 **Session ID 被窃取会怎样?如何防范?** 攻击者可用截获的 Session ID 冒充用户(Session 劫持)。防范:Cookie 标记 HttpOnly+Secure,绑定 IP/UA 校验,使用 SameSite 属性,Session 定期轮换 ID,设置合理超时。 **分布式系统中 Session 如何共享?** 三种方案:1) 粘性 Session(Nginx ip_hash),简单但不利于负载均衡;2) 集中存储(Redis/Memcached),最常用;3) Session 复制(Tomcat 集群广播),数据量大时性能差。 **JWT 相比 Session 的优劣?** 优势:无状态,服务端不存数据,天然支持分布式。劣势:无法主动注销(需黑名单机制又回到有状态),Token 体积大于 Session ID,续期机制复杂(双 Token 方案)。 **Cookie 被禁用怎么办?** Session ID 可通过 URL 参数传递(;jsessionid=xxx),但会暴露在日志和 Referer 中,安全性差。更推荐在登录页提示用户启用 Cookie。 ## 写段代码 ```javascript // Express 中 Session 存入 Redis const session = require('express-session'); const RedisStore = require('connect-redis'); app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret', resave: false, cookie: { httpOnly: true, secure: true, maxAge: 3600000 } })); ```
服务端5月29日 01:21
Session Cookie 和 Persistent Cookie 有什么区别?Session Cookie 不设 Expires 和 Max-Age 属性,浏览器将其存在内存中,关闭浏览器即消失;Persistent Cookie 设有明确的过期时间,存储在磁盘上,过期前跨会话持久存在。服务端通过 Set-Cookie 响应头控制类型:不设过期属性是 Session Cookie,设 Max-Age 或 Expires 则为 Persistent Cookie。安全层面两者都应标记 HttpOnly 和 Secure,Persistent Cookie 因长期驻留磁盘更易被窃取,敏感数据应避免持久化。SameSite 属性(Strict/Lax/None)对两者同样适用,防止 CSRF 攻击。 ## 追问 **浏览器关闭后 Session Cookie 一定被清除吗?** 不一定。现代浏览器有恢复会话功能(如 Chrome 的继续浏览上次打开的网页),会将 Session Cookie 写入磁盘以便恢复,实际表现等同于 Persistent Cookie。依赖关闭即清除做安全假设是不可靠的。 **Max-Age 和 Expires 有什么区别?** Max-Age 是相对秒数,从收到 Cookie 时起算;Expires 是绝对时间点,依赖客户端时钟。Max-Age 优先级更高(RFC 6265),两者都不设则为 Session Cookie。 **Session Cookie 能存多少数据?** 和 Persistent Cookie 一样受 4KB 限制,区别仅在于生命周期,不在容量。大量数据应改用 localStorage 或 IndexedDB。 **如何强制 Session Cookie 在标签页关闭时清除?** 无法可靠实现。可用的替代方案:用 sessionStorage(标签页级)或服务端维护有效期并主动使 Session 失效。 ## 写段代码 ```javascript // 服务端设置两种 Cookie res.setHeader('Set-Cookie', [ 'sid=abc123; HttpOnly; Secure; SameSite=Lax', // Session Cookie 'theme=dark; Max-Age=31536000; HttpOnly; SameSite=Lax' // Persistent Cookie ]); ```
服务端5月28日 02:15
Cookie 的 Expires 和 Max-Age 有什么区别?## 核心结论 **Max-Age 优先于 Expires**。两者同时设置时,浏览器只认 Max-Age。Expires 是 HTTP/1.0 的产物,用绝对时间;Max-Age 是 HTTP/1.1 引入的,用相对秒数,不受客户端时钟偏差影响。现代开发应优先使用 Max-Age,仅在需要兼容 IE 时同时设置 Expires 作为降级。 ## 两者的本质区别 | 维度 | Expires | Max-Age | |------|---------|---------| | 规范来源 | HTTP/1.0 (Netscape 草案) | HTTP/1.1 (RFC 6265) | | 时间类型 | 绝对时间 (GMT 字符串) | 相对时间 (秒数) | | 优先级 | 低 | 高 | | 时钟依赖 | 依赖客户端本地时间 | 不依赖,从收到 Cookie 起算 | | 浏览器兼容 | 全部浏览器 | IE6/7/8 不支持 | | 典型值 | `Wed, 09 Jun 2026 10:18:14 GMT` | `3600` | **关键差异在于时钟依赖**:Expires 指定“到什么时刻过期”,依赖客户端系统时钟。如果用户电脑时间比服务器快几小时,Cookie 可能提前过期;慢几小时则延迟过期。Max-Age 指定“从现在起还能活多久”,浏览器收到 Cookie 后自己倒计时,不受时钟偏移影响。 ## Expires 的用法 ```javascript // 设置 7 天后过期 const expires = new Date(); expires.setDate(expires.getDate() + 7); document.cookie = "token=abc; Expires=" + expires.toUTCString(); // 设置过去的日期 → 立即删除 Cookie document.cookie = "token=abc; Expires=Thu, 01 Jan 1970 00:00:00 GMT"; ``` 时间格式必须是 UTC (GMT) 字符串。`toUTCString()` 生成正确格式,`toISOString()` 生成的格式浏览器不一定识别。 ## Max-Age 的用法 ```javascript // 1 小时后过期 document.cookie = "token=abc; Max-Age=3600"; // 1 天后过期 document.cookie = "token=abc; Max-Age=86400"; // 立即删除 document.cookie = "token=abc; Max-Age=0"; // 会话 Cookie(浏览器关闭即删除)——不设置 Max-Age 和 Expires 即可 document.cookie = "sessionId=abc"; ``` **Max-Age 的特殊值**:`0` 表示立即删除;负数也表示立即删除(但部分浏览器行为不一致,推荐用 `0`);不设置则变成会话 Cookie。 ## 优先级规则 两者同时出现时,**Max-Age 优先**: ```javascript // 同时设置,Max-Age 生效,Cookie 在 1 小时后过期 document.cookie = "token=abc; Expires=Wed, 09 Jun 2026 10:18:14 GMT; Max-Age=3600"; ``` 这个优先级是 RFC 6265 明确规定的。IE6/7/8 不识别 Max-Age,会回退使用 Expires——这也是为什么兼容旧浏览器时要同时设置两者。 ## 追问:服务器端怎么设置? 前端 `document.cookie` 只能设置非 HttpOnly 的 Cookie。实际项目中 Cookie 过期时间主要由服务端通过 `Set-Cookie` 响应头控制: ```http Set-Cookie: token=abc; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Strict ``` Node.js Express 示例: ```javascript res.cookie("authToken", token, { maxAge: 3600 * 1000, // 注意:Express 的 maxAge 单位是毫秒 httpOnly: true, secure: true, sameSite: "strict", path: "/" }); ``` **注意坑**:Express 的 `maxAge` 选项单位是毫秒,而 HTTP 头中的 `Max-Age` 单位是秒。Express 内部会自动转换。 ## 追问:删除 Cookie 的正确姿势 删除 Cookie 必须满足三个条件:名称、域名(Domain)、路径(Path) 全部匹配,否则浏览器会创建一个同名新 Cookie 而非删除旧的。 ```javascript // 删除时必须与设置时的 Path 和 Domain 一致 document.cookie = "token=abc; Max-Age=0; Path=/; Domain=example.com"; ``` ## 实际开发建议 - **优先用 Max-Age**:不受客户端时钟影响,计算简单,是 RFC 6265 推荐方式 - **兼容旧浏览器时同时设置两者**:Max-Age 为主,Expires 做降级 - **敏感信息用短过期**:认证 Token 建议几小时级别,配合刷新机制 - **“记住我”用长过期**:30 天左右,但务必配合 HttpOnly + Secure + SameSite - **不要依赖 Expires 做精确过期**:用户设备时钟不可控
服务端5月28日 02:07
Cookie 在跨域场景下如何使用?需要注意哪些问题?## 核心回答 Cookie 默认受同源策略约束,只在同源请求中发送。要实现跨域携带 Cookie,需要前后端同时配置:前端设置 `credentials: 'include'`,后端返回 `Access-Control-Allow-Credentials: true` 且 `Access-Control-Allow-Origin` 不能为 `*`。此外,Chrome 80+ 默认将 SameSite 设为 Lax,跨站场景下必须显式设置 `SameSite=None; Secure`。 ## 同源策略对 Cookie 的限制 浏览器同源策略要求协议、域名、端口三者完全一致。Cookie 的"源"判断比脚本更宽松——它只看 Domain 和 Path,不区分端口和协议。但跨域请求(如 `frontend.com` 向 `api.com` 发请求)默认不会携带 Cookie,这是浏览器的安全行为。 ## 跨域携带 Cookie 的配置方案 ### 前端:声明携带凭证 ```javascript // fetch 方式 fetch('https://api.example.com/data', { credentials: 'include' // 跨域时发送 Cookie }); // XMLHttpRequest 方式 const xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open('GET', 'https://api.example.com/data'); xhr.send(); ``` `credentials: 'include'` 表示无论同源还是跨域都发送 Cookie。如果只想同源发送,用 `'same-origin'`。 ### 后端:允许接收凭证 ```javascript // Express + cors 中间件 app.use(cors({ origin: 'https://frontend.example.com', // 必须是具体域名,不能是 * credentials: true })); // 设置 Cookie res.setHeader('Set-Cookie', [ 'token=xyz; HttpOnly; Secure; SameSite=None; Path=/' ]); ``` 三个关键约束: - `Access-Control-Allow-Credentials: true`,否则浏览器忽略响应中的 Cookie - `Access-Control-Allow-Origin` 必须是具体域名,`*` 会导致浏览器拒绝携带 Cookie 的请求 - Cookie 本身需要 `Secure` 和 `SameSite=None`(见下文) ## SameSite 属性详解 SameSite 是 Cookie 最重要的跨域相关属性,控制 Cookie 在跨站请求中是否发送: | 值 | 行为 | 典型场景 | |---|---|---| | **Strict** | 仅同站请求发送,跨站导航也不发送 | 银行、支付等高安全场景 | | **Lax**(Chrome 80+ 默认) | 同站请求 + 顶级导航的 GET 请求发送 | 大多数场景的默认选择 | | **None** | 跨站请求也发送,必须搭配 Secure | 跨域 API 认证、SSO | Chrome 80 之前 SameSite 默认是 None,之后改为 Lax。这意味着如果你的 Cookie 没有显式声明 SameSite,跨站的 POST 请求、iframe 内请求、Ajax 请求都不会携带 Cookie。 实际影响:如果你的前端部署在 `app.com`,API 部署在 `api.com`,这两个属于跨站(cross-site),必须设置 `SameSite=None; Secure`。 ## Domain 属性实现子域共享 如果只是子域名之间共享 Cookie(如 `a.example.com` 和 `b.example.com`),不需要走 CORS,用 Domain 属性即可: ```javascript document.cookie = "token=xyz; Domain=.example.com; Path=/"; ``` 设置 `Domain=.example.com` 后,所有子域名都能读取该 Cookie。注意 Domain 只能设为当前域名的父域或自身,不能跨顶级域。 ## 第三方 Cookie 的淘汰趋势 主流浏览器正在逐步淘汰第三方 Cookie: - **Safari**:早已默认阻止所有第三方 Cookie(ITP 策略) - **Chrome**:从 Chrome 115 开始为第三方 Cookie 引入 Storage Access API 限制,计划全面淘汰(时间线多次调整,目前仍在推进) - **Firefox**:默认阻止已知跟踪器的第三方 Cookie(ETP 策略) 替代方案正在发展中: - **Cookie Partitioning(CHIPS)**:通过 `Partitioned` 属性让第三方 Cookie 按顶级站点隔离,Chrome 已开始支持 - **Storage Access API**:允许用户授权特定第三方访问 Cookie - **First-Party Set**:同一组织下的域名声明为第一方集合 如果你的业务依赖第三方 Cookie(如 SSO、嵌入式登录),需要尽早关注这些变化。 ## 预检请求与 Cookie 跨域请求如果使用了非简单方法或自定义 Header,浏览器会先发 OPTIONS 预检请求。预检请求本身不会携带 Cookie,但服务器需要在预检响应中包含 `Access-Control-Allow-Credentials: true`,否则后续的实际请求携带的 Cookie 会被浏览器拒绝。 ```javascript // 预检请求的响应也需要配置 app.options('/api/*', cors({ origin: 'https://frontend.example.com', credentials: true, methods: ['GET', 'POST', 'PUT'], allowedHeaders: ['Content-Type', 'Authorization'] })); ``` ## 安全防护要点 跨域 Cookie 场景下安全风险更高,务必注意: 1. **必须设置 HttpOnly**:防止 XSS 窃取 Cookie 2. **必须设置 Secure**:`SameSite=None` 强制要求,确保只在 HTTPS 下传输 3. **验证 Origin 和 Referer**:后端应校验请求来源,防止非法域名的跨域请求 4. **CSRF 防护**:即使有 SameSite,仍建议配合 CSRF Token,因为 SameSite=Lax 对顶级导航 GET 请求仍会放行 5. **设置合理的过期时间**:避免长期有效的 Cookie 被盗用 ## 面试追问方向 - **SameSite 从 None 改为 Lax 后,哪些请求受影响?** —— 跨站 POST、iframe 内请求、subresource 请求不再携带 Cookie,但顶级导航的 GET(如 `<a>` 链接点击)仍会发送。 - **CORS 为什么不允许 `Access-Control-Allow-Origin: *` 搭配 credentials?** —— 因为 `*` 表示任何域名都可携带凭证访问,这等于完全绕过了同源策略的保护,浏览器的安全模型不允许。 - **Cookie 的 Domain 和 CORS 的 Origin 有什么区别?** —— Domain 控制 Cookie 的作用域(哪些域名能访问),Origin 是请求头中的来源标识。两者判断"同源/跨站"的标准不同:Cookie 看注册域名(eTLD+1),CORS 看协议+域名+端口。
服务端5月28日 01:18
Cookie 和 LocalStorage 有什么区别?什么场景该用哪个?## 核心答案 Cookie 和 LocalStorage 都是浏览器的客户端存储机制,核心区别在于:Cookie 每次请求自动携带到服务器,容量约 4KB,适合存服务端需要的数据(如会话 ID);LocalStorage 不参与网络请求,容量 5-10MB,适合纯客户端的持久化数据(如用户偏好)。 两者选择的关键判断标准是:**数据是否需要服务端读取**。需要则 Cookie,不需要则 LocalStorage。 ## 容量与生命周期 | 维度 | Cookie | LocalStorage | |------|--------|-------------| | 容量 | 单条约 4KB,每域名 50 个左右 | 每域名 5-10MB | | 生命周期 | 可设 Expires/Max-Age,也支持会话级(浏览器关闭即失效) | 永久存储,除非手动清除 | | 作用域 | Domain + Path 精确控制,可跨子域 | 严格同源(协议+域名+端口) | ## 网络行为差异 Cookie 最本质的特征是**自动随 HTTP 请求发送**。这意味着: - 优势:服务端无需额外代码即可读取,适合身份认证、个性化等场景 - 劣势:每次请求都携带,增加带宽开销;大量冗余数据会拖慢请求 LocalStorage 的数据**完全留在客户端**,不会自动发送: - 优势:不占用请求带宽,存储空间大 - 劣势:服务端无法直接获取,需要 JS 读取后通过请求体或 Header 手动传递 ## API 对比 ```javascript // Cookie —— 字符串拼接,解析麻烦 document.cookie = "token=abc123; path=/; max-age=3600; secure; samesite=strict"; // 读取需要手动解析 document.cookie 字符串 // LocalStorage —— 键值对 API,直观简洁 localStorage.setItem("theme", "dark"); localStorage.getItem("theme"); // "dark" localStorage.removeItem("theme"); localStorage.clear(); ``` 实际项目中操作 Cookie 推荐使用 `js-cookie` 等库,避免手写解析逻辑。 ## 安全性 这是面试高频追问点: - **XSS 防护**:Cookie 可设置 `HttpOnly` 标志,阻止 JS 读取,即使页面被注入恶意脚本也无法窃取 Cookie 内容;LocalStorage 没有此机制,任何 XSS 漏洞都能读取全部数据 - **CSRF 防护**:Cookie 自动携带导致容易遭受 CSRF 攻击,需配合 `SameSite` 属性或 CSRF Token 防御;LocalStorage 不自动发送,天然免疫 CSRF - **传输安全**:Cookie 可设 `Secure` 标志确保仅 HTTPS 下发送;LocalStorage 无此属性,但数据本身不参与传输 `SameSite` 属性三个值: - `Strict`:完全禁止跨站发送,最安全但影响体验(从外部链接跳转不带 Cookie) - `Lax`:导航到目标网站的 GET 请求允许携带,是现代浏览器默认值 - `None`:允许跨站发送,必须同时设置 `Secure` ## 使用场景决策 **选 Cookie 的场景:** - 会话管理:登录态 Session ID / JWT Refresh Token - 服务端需要读取的偏好:语言、地区 - CSRF 防护 Token 的双提交模式 - A/B 测试分桶标识 **选 LocalStorage 的场景:** - 用户偏好:主题、布局、字号 - 业务缓存:接口数据快照、表单草稿 - 离线应用数据:PWA 缓存资源 - 第三方库配置:编辑器工具栏状态 **现代认证的混合方案(2025 实践):** 1. 短期 Access Token 存在内存中(5-15 分钟过期) 2. 长期 Refresh Token 存在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中 3. LocalStorage 仅存非敏感的用户偏好 这种方案兼顾了 XSS 和 CSRF 两方面的安全防护。 ## 追问 **1. Cookie、LocalStorage、SessionStorage 三者怎么选?** SessionStorage 与 LocalStorage API 完全一致,区别在于生命周期——标签页关闭即清除。适合临时性数据:表单填写进度、单次会话的搜索条件、多步骤向导的状态。 **2. Cookie 的 SameSite 属性解决了什么问题?默认值是什么?** 解决 CSRF 攻击问题。Chrome 80+ 默认值为 `Lax`,允许顶级导航的 GET 请求携带 Cookie,但阻止跨站 POST 请求携带。 **3. 如果 LocalStorage 被 XSS 攻击读取了怎么办?** 核心原则:LocalStorage 中不要存敏感数据(密码、Token)。已被窃取的数据无法撤回,只能立即让用户重置密码、吊销旧 Token。防御重点在前端输入过滤和 CSP 策略。 **4. 超过 LocalStorage 5MB 限制会怎样?** 写入操作抛出 `QuotaExceededError` 异常。实际项目中应做好 try-catch 和容量监控,或在写入前用 `JSON.stringify(data).length` 估算占用。 **5. 跨域下 Cookie 和 LocalStorage 的表现有什么不同?** Cookie 可通过设置 `Domain=.example.com` 实现主域名下子域共享;LocalStorage 严格同源,不同子域各自独立,跨域共享需要 postMessage 等方案中转。
服务端5月28日 00:41
如何使用 Cookie 实现"记住我"功能?需要注意哪些安全问题?## 核心答案 Cookie 实现"记住我"的核心思路是:登录成功后生成一个加密的长期 Token,存入设置了 HttpOnly + Secure + SameSite 的持久化 Cookie,服务端同时将 Token 哈希存入数据库。下次访问时浏览器自动携带 Cookie,服务端校验 Token 哈希完成自动登录,无需用户再次输入密码。 关键安全原则有三条: - **永远不要在 Cookie 中存储明文密码或密码哈希**,只用随机生成的不可预测 Token - **每次使用后轮换 Token**,旧的立即失效,防止重放攻击 - **Cookie 必须设置 HttpOnly + Secure + SameSite=Strict**,堵住 XSS 窃取、中间人截获、CSRF 伪造三条攻击路径 ## 实现方案对比 ### 方案一:持久 Session Cookie 最简单的方式——延长 Session Cookie 的过期时间: ```javascript // 服务端设置(Node.js Express) function setRememberMeCookie(res, token, rememberMe) { const options = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/' }; if (rememberMe) { // 勾选"记住我":30天有效 options.maxAge = 30 * 24 * 60 * 60 * 1000; } else { // 未勾选:会话 Cookie,浏览器关闭即失效 delete options.maxAge; } res.cookie('authToken', token, options); } ``` 优点:实现简单,适用于小型应用。 缺点:Token 不轮换,一旦泄露可被长期使用;单 Token 承载所有功能,撤销困难。 ### 方案二:双令牌机制(推荐) 将短期的访问令牌和长期的刷新令牌分离: ```javascript const crypto = require('crypto'); const jwt = require('jsonwebtoken'); function generateTokens(userId) { // 访问令牌:短期,用于接口鉴权 const accessToken = jwt.sign( { userId }, process.env.JWT_SECRET, { expiresIn: '15m' } ); // 刷新令牌:长期,仅用于换取新的访问令牌 const refreshToken = crypto.randomBytes(32).toString('hex'); // 服务端存储刷新令牌的哈希值(不是明文) const tokenHash = crypto .createHash('sha256') .update(refreshToken) .digest('hex'); await db.saveRefreshToken({ userId, tokenHash, // 只存哈希,数据库泄露也无法还原 Token expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), userAgent: req.headers['user-agent'], ipAddress: req.ip }); return { accessToken, refreshToken }; } // 设置 Cookie function setAuthCookies(res, tokens, rememberMe) { res.cookie('accessToken', tokens.accessToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 * 60 * 1000 // 15分钟 }); if (rememberMe) { res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 30 * 24 * 60 * 60 * 1000 // 30天 }); } } ``` 优点:访问令牌短命即使泄露影响有限,刷新令牌可单独撤销,支持多设备管理。 缺点:实现复杂度更高,需要额外的刷新接口和存储。 ## 安全防护要点 ### Token 生成:必须用加密安全随机数 ```javascript // 正确:crypto.randomBytes const token = crypto.randomBytes(32).toString('hex'); // 错误:Math.random 或时间戳——可预测,可被暴力破解 const badToken = Date.now().toString(36) + Math.random().toString(36); ``` `Math.random()` 是伪随机数,攻击者可以通过观察输出模式预测后续值。`crypto.randomBytes()` 使用操作系统提供的真随机源,不可预测。 ### Token 存储:数据库只存哈希 数据库中存储 Token 的 SHA-256 哈希,而非明文。这样即使数据库被拖库,攻击者也无法用哈希反推原始 Token 伪造 Cookie。 ```javascript // 存储 const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); await db.save({ tokenHash, userId, expiresAt }); // 验证 const inputHash = crypto.createHash('sha256').update(cookieToken).digest('hex'); const record = await db.findOne({ tokenHash: inputHash }); ``` ### Token 轮换:用一次换一个 每次用刷新令牌换新的访问令牌时,同时生成新的刷新令牌,旧的立即删除: ```javascript async function rotateRefreshToken(oldToken, req) { const inputHash = crypto.createHash('sha256').update(oldToken).digest('hex'); const record = await db.findOne({ tokenHash: inputHash }); if (!record || record.expiresAt < new Date()) { throw new Error('Invalid or expired token'); } // 异地登录检测:User-Agent 或 IP 变化时告警 if (record.userAgent !== req.headers['user-agent']) { // 可选:通知用户,或要求重新验证 await notifyUser(record.userId, '检测到新设备登录'); } // 删除旧令牌 await db.deleteOne({ tokenHash: inputHash }); // 生成新令牌 const newToken = crypto.randomBytes(32).toString('hex'); const newHash = crypto.createHash('sha256').update(newToken).digest('hex'); await db.save({ userId: record.userId, tokenHash: newHash, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), userAgent: req.headers['user-agent'], ipAddress: req.ip }); return newToken; } ``` 不轮换的后果:攻击者偷走 Token 后可以无限期使用,用户改密码也不会失效。 ### Cookie 属性:三件套缺一不可 | 属性 | 作用 | 不设置的后果 | |------|------|-------------| | HttpOnly | 禁止 JS 读取 Cookie | XSS 攻击可通过 document.cookie 窃取令牌 | | Secure | 仅 HTTPS 传输 | HTTP 明文传输,中间人可直接截获 | | SameSite=Strict | 跨站请求不携带 Cookie | CSRF 攻击可伪造用户操作 | ### 撤销与清理 用户主动登出或修改密码时,必须清除所有刷新令牌: ```javascript async function revokeAllTokens(userId) { await db.deleteMany({ userId }); // 清除客户端 Cookie res.clearCookie('accessToken'); res.clearCookie('refreshToken'); } // 修改密码后强制所有设备重新登录 async function changePassword(userId, newPassword) { await updateUserPassword(userId, newPassword); await revokeAllTokens(userId); } ``` ## 客户端自动登录流程 ```javascript // 页面加载时尝试自动登录 async function checkAutoLogin() { // 浏览器自动携带 HttpOnly Cookie,无需手动读取 const response = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' // 确保携带 Cookie }); if (response.ok) { const { accessToken } = await response.json(); // 短期访问令牌可存内存(或 sessionStorage),不放 localStorage return accessToken; } // 刷新失败,跳转登录页 window.location.href = '/login'; return null; } ``` 注意:访问令牌不要存 `localStorage`,因为 XSS 可以直接读取。存在内存变量或 `sessionStorage` 中更安全。 ## 面试追问 **Q1:Cookie 的 SameSite 设为 Strict 会不会影响从外部链接跳转过来的自动登录?** 会。SameSite=Strict 意味着任何跨站请求都不带 Cookie,包括从搜索引擎、邮件链接点进来。如果需要兼顾体验,可以用 SameSite=Lax(GET 请求仍携带 Cookie),再配合 CSRF Token 做双重保护。 **Q2:刷新令牌被偷了怎么办?** 轮换机制本身就在降低风险——旧令牌用一次就作废。更完善的方案是记录每个令牌的 IP 和 User-Agent,发现异常变化时:通知用户、要求二次验证、或直接撤销该用户所有令牌。 **Q3:为什么不用 JWT 直接做"记住我"?** JWT 一旦签发就无法撤销(除非引入黑名单,但那就失去了无状态的优势)。长期有效的 JWT 泄露后攻击者可以一直使用到过期。用不透明 Token + 服务端存储 + 轮换机制,撤销只需要删一条数据库记录。 **Q4:多设备同时登录怎么管理?** 每台设备生成独立的刷新令牌,数据库记录每条令牌的设备信息(User-Agent、IP、最后使用时间)。用户可以在"已登录设备"页面查看并逐个撤销。