项目复习第二天:从 Token 存储风险理解 XSS、CSRF 与 CORS
复习日期:2026-09-14
当前主线:认证安全------登录闭环
一、为什么登录模块会遇到 XSS、CSRF 和 CORS
第一天复习登录模块时,我已经理解了 Access Token、Refresh Token、Redis jti 白名单、Rotation 和登出空窗期。继续讨论前端令牌存储时,又出现了一个新的问题:当前项目把 Access Token 和 Refresh Token 都保存在 localStorage 中,这样虽然实现简单,但需要考虑浏览器环境中的安全风险。
当前项目的前端会从 localStorage 读取 Access Token,并主动加入请求头:
Authorization: Bearer <access-token>
这让我需要区分三个经常同时出现、但职责完全不同的概念:
-
XSS:攻击者的代码进入目标网站并执行。
-
CSRF:攻击者借助浏览器自动携带的登录凭证执行操作。
-
CORS:浏览器控制跨源页面能否读取接口响应。
它们都与浏览器安全有关,但 XSS 和 CSRF 是攻击方式,CORS 是浏览器的跨源访问控制机制。
二、XSS:攻击者的代码进入了目标网站
XSS 全称是 Cross-Site Scripting,即跨站脚本攻击。
它的核心不是"攻击者从外部调用了接口",而是攻击者设法让恶意 JavaScript 在知享页面中运行。代码虽然由攻击者编写,但浏览器会把它当成知享网站自身的代码。
例如,如果一个恶意脚本能够在当前页面中执行,它可能读取:
localStorage.getItem("zhiguang_auth_tokens")
当前项目把 Access Token 和 Refresh Token 都存放在这个位置。因此一旦发生 XSS,恶意脚本可能读取双令牌,将它们发送给攻击者,或者直接调用知享接口冒充当前用户。
XSS 脚本还可能:
-
修改页面显示内容;
-
读取用户在页面中输入的信息;
-
调用发布、修改资料等业务接口;
-
读取非
HttpOnlyCookie; -
窃取页面中其他可以被 JavaScript 访问的数据。
React 默认会转义普通文本,但这不代表 React 项目天然不会出现 XSS。不安全的 HTML 渲染、DOM 操作、第三方脚本或其他注入入口仍然可能产生风险。
三、CSRF:攻击者借用了浏览器自动携带的身份
CSRF 全称是 Cross-Site Request Forgery,即跨站请求伪造。
它通常与 Cookie 认证相关,因为 Cookie 可能由浏览器自动携带。攻击者不一定能读取用户的 Cookie,但可以诱导用户浏览器向目标网站发送请求。
典型过程是:
用户已经登录知享,浏览器保存了登录 Cookie
→ 用户访问 evil.com
→ evil.com 诱导浏览器向知享发送修改资料或注销请求
→ 浏览器可能自动携带知享的 Cookie
→ 后端误以为这是用户主动执行的操作
CSRF 的目标通常是让请求产生副作用。攻击者不一定需要看到响应内容,只要修改资料、退出登录或其他敏感操作已经执行,攻击目的就可能达到。
当前项目主要使用 localStorage + Authorization。攻击者网站不能直接读取知享域名下的 localStorage,也就不知道用户的 Access Token,无法主动构造正确的 Authorization 请求头。因此,这种方式不像 Cookie 认证那样容易受到传统 CSRF,但主要风险会转向 XSS 窃取 Token。
四、CORS:控制跨源页面能否读取响应
CORS 全称是 Cross-Origin Resource Sharing,即跨源资源共享。
一个源由三部分组成:
源 = 协议 + 域名 + 端口
只要其中一项不同,就属于不同源。例如:
https://zhiguang.example
https://evil.example
以及:
http://localhost:5173
http://localhost:8080
浏览器默认通过同源策略限制一个页面读取其他源的数据。CORS 是服务器通过响应头告诉浏览器,哪些来源的 JavaScript 可以读取自己的响应。
CORS 不是:
-
用户认证机制;
-
接口授权机制;
-
服务端防火墙;
-
XSS 防护;
-
完整的 CSRF 防护。
CORS 主要限制攻击者页面是否能够读取响应,但不能保证所有跨源请求都不会发送。某些请求可能已经携带 Cookie 到达后端并产生副作用,只是浏览器最终不允许攻击者页面读取响应。
curl、Postman 和服务端程序也不受浏览器 CORS 规则限制,因为 CORS 是浏览器执行的安全机制。
五、一次跨源判断的误区
考虑这样一个场景:恶意代码运行在 evil.com,它读不到知享的 localStorage,但用户浏览器会自动携带知享的登录 Cookie。攻击者诱导浏览器向知享发送修改资料请求。
这个场景属于 CSRF。但我最开始认为:"这是用户本地浏览器发送的请求,所以没有跨源。"
这个判断是错误的。请求是不是跨源,不取决于它是不是由用户本地浏览器发送,而取决于发起请求的页面源与目标接口源是否相同。
这里的关系是:
发起页面源:evil.com
目标接口源:zhiguang.example
两个源不同,所以请求仍然是跨源请求。
纠正后的理解是:
CORS 主要决定攻击者页面能否读取知享接口的响应,但请求可能已经携带 Cookie 到达后端。即使攻击者读不到响应,退出登录、修改资料等副作用也可能已经产生。
六、HttpOnly 为什么只能降低 XSS 危害
当 Refresh Token 已经保存在 HttpOnly Cookie 中,但知享页面仍然发生 XSS 时,恶意 JavaScript 不能直接读取和复制 Refresh Token,但仍然可以以当前用户身份调用接口,在被攻击的页面中发送请求并冒充用户执行操作。
这说明 HttpOnly 解决的是 Cookie 内容被 JavaScript 直接读取的问题,而不是阻止页面中的恶意脚本执行操作。
HttpOnly 保护的是 Cookie 内容的机密性:JavaScript 不能通过 document.cookie 直接读取和复制 Refresh Token。但是浏览器仍然可以在符合条件的请求中自动携带 Cookie。因此,XSS 脚本虽然难以把 Refresh Token 复制到攻击者自己的设备,却仍然可以在当前页面中调用修改资料、发布内容或刷新 Token 等接口。
所以:
HttpOnly可以降低 Refresh Token 被 XSS 直接窃取的风险,但不能阻止恶意脚本在当前登录页面中操作用户账号。
七、XSS、CSRF 与 CORS 的最终区分
| 概念 | 攻击代码的位置 | 利用的条件 | 主要风险 |
|---|---|---|---|
| XSS | 目标网站页面内部 | 网站执行了恶意脚本 | 读取 Token、修改页面、冒充用户 |
| CSRF | 攻击者网站 | 浏览器自动携带 Cookie | 借用户身份执行有副作用的操作 |
| CORS | 不是攻击类型 | 浏览器的跨源读取规则 | 决定跨源 JavaScript 能否读取响应 |
可以用下面三句话快速记忆:
-
XSS 是攻击者已经混进了目标网站。
-
CSRF 是攻击者借用了用户浏览器携带的身份。
-
CORS 是浏览器控制外部页面能否读取目标网站的响应。
对于当前项目:
双令牌存入 localStorage
→ 主要关注 XSS 读取 Token
Refresh Token 改为 HttpOnly Cookie
→ 降低 Refresh Token 被直接读取的风险
→ 但需要额外关注 CSRF
八、Spring Security 认证链路
回到登录闭环后,当前对 Access Token 如何经过 Spring Security 到达 Controller 的理解如下:
目前已经确认的内容是:
-
Access Token 由
JwtDecoder验签。 -
permitAll()不等于跳过整个安全过滤器链。 -
401 表示请求没有完成认证,例如 Token 缺失、过期、格式错误或签名错误。
-
403 表示认证已经成功,但当前用户权限不足。
目前对Spring Security 认证完整链路的理解不完整,以下是Access Token 经过 Spring Security 到达 Controller 的完整流程:
请求携带 Authorization: Bearer <Access Token>
→ 进入 SecurityFilterChain
→ Spring Security 提取 Bearer Token
→ JwtDecoder 校验 Token
→ 校验成功后创建 Authentication
→ 将 Authentication 放入 SecurityContext
→ 执行授权判断
→ 通过后执行 Controller
九、第二天复习结果
已达到基础复述要求
-
XSS 的执行位置以及它对
localStorageToken 的风险。 -
CSRF 利用浏览器自动携带 Cookie 产生副作用。
-
CORS 主要控制跨源响应读取,不能替代 CSRF 防护。
-
HttpOnly 能降低 Refresh Token 被直接读取的风险,但不能彻底阻止 XSS 操作用户账号。
-
401 与 403 的基本区别。
-
permitAll()不等于跳过整个安全过滤器链。