当前位置: 首页 > news >正文

浏览器安全机制与现代 SPA 认证架构深度解析

一、同源策略(SOP):一切的起点

1.1 什么是同源

浏览器用协议 + 主机 + 端口三元组定义"源"( Origin ):

URL https://app.example.com 的关系
https://app.example.com/page ✅ 同源
http://app.example.com ❌ 协议不同
https://api.example.com ❌ 主机不同
https://app.example.com:8080 ❌ 端口不同

1.2 SOP 保护的是什么

SOP(Same-Origin Policy)的核心规则:页面中的 JS 只能读取与自身同源的响应

没有 SOP 会发生什么:

用户已登录 bank.com(浏览器持有 bank.com 的 Cookie)↓
用户访问 evil.com↓
evil.com 的 JS 向 bank.com/api/transfer 发请求
浏览器自动携带 bank.com 的 Cookie↓
没有 SOP → JS 读到账户数据,完成转账 ← 攻击成功
有  SOP → JS 读不到响应内容           ← 攻击失败

SOP 是浏览器一切安全机制的基础,CORS 和 Cookie 的 SameSite 属性都在它之上构建。


二、CORS:在 SOP 上有选择地开口

2.1 CORS 的本质

CORS(Cross-Origin Resource Sharing)不是"关闭 SOP",而是服务器通过响应头主动声明允许哪些外部来源读取自己的响应。

关键认知:CORS 是浏览器行为,不是服务器行为。

  • 服务器只负责在响应头里写声明
  • 浏览器决定是否放行,服务器无法控制浏览器跳过检查
  • 非浏览器环境(curl、Postman、服务端代码)完全没有 CORS

2.2 简单请求与预检请求

浏览器将跨源请求分为两 类 ,处理逻辑不同:

简单请求(满足以下全部条件):

  • 方法:GET / POST / HEAD
  • Content-Type 仅限:text/plain / application/x-www-form-urlencoded / multipart/form-data
  • 无自定义请求头
简单请求流程:浏览器                              服务器│── GET /api/data                    ││   Origin: https://app.example.com→ │  ← 请求已到达服务器并执行│                                    ││ ←─ 200 OK ─────────────────────── ││    Access-Control-Allow-Origin:    ││    https://app.example.com         ││                                    │├─ ACAO 匹配 → JS 可读响应 ✅        │└─ ACAO 缺失 → 浏览器拦截响应 ❌     │

⚠️ 简单请求无论 CORS 结果如何,请求都已经发到服务器执行。CORS 只控制 JS 能否读响应,无法阻止请求本身(这也是为什么防 CSRF 不能只靠 CORS)。

预检请求(不满足简单请求条件,如 application/jsonAuthorization 头、PUT/DELETE 方法):

预检请求流程:浏览器                                        服务器│── OPTIONS /api/data(预检)──────────────→ ││   Origin: https://app.example.com          ││   Access-Control-Request-Method: POST      ││   Access-Control-Request-Headers: Authorization│                                            ││ ←─ 204 No Content ───────────────────────  ││    Access-Control-Allow-Origin: https://app.example.com│    Access-Control-Allow-Methods: GET, POST, PUT│    Access-Control-Allow-Headers: Authorization, Content-Type│    Access-Control-Max-Age: 7200            ││                                            │├─ 预检通过 → 发实际请求 ✅                   │└─ 预检失败 → 实际请求不发送 ❌               │

预检与简单请求的本质区别:预检失败时,实际请求根本不会发出

2.3 携带凭证的跨源请求

默认情况下,跨源请求不携带 Cookie。需要同时满足两个条件才能带上:

// 前端:显式声明携带凭证
fetch('https://api.example.com/data', {credentials: 'include'
});
# 服务器响应头:两个条件缺一不可
Access-Control-Allow-Origin: https://app.example.com  # 不能是 *
Access-Control-Allow-Credentials: true

2.4 如何合法避免预检

方案 原理 适用场景
设计成简单请求 避免使用触发预检的方法/头 API 设计阶段
Max-Age 缓存预检结果 有效期内不重复预检 通用优化(Chrome 上限 2 小时)
反向代理同源化 Nginx 统一域名,浏览器认为同源 最彻底,推荐
BFF 后端代理 浏览器只访问自家服务端,服务端无 CORS 有后端的项目

三、Cookie:存储与发送规则

浏览器收到 Set-Cookie 时,用域名匹配算法决定是否保存:

响应的 host 必须与 Domain 属性满足域名匹配关系:host 等于 Domain,或者 host 以 .Domain 结尾。

响应来自 api.example.com:Set-Cookie: sid=abc; Domain=example.com→ api.example.com 以 .example.com 结尾 ✅ 保存→ 发送范围:example.com 及所有子域Set-Cookie: sid=abc; Domain=other.com→ api.example.com 与 other.com 无关 ❌ 拒绝Set-Cookie: sid=abc; Domain=sub.api.example.com→ api.example.com 不以 .sub.api.example.com 结尾 ❌ 拒绝(父域无法给子域设置 cookie)Set-Cookie: sid=abc(不带 Domain)→ Host-Only 模式:仅 api.example.com 本身能收到→ 子域 sub.api.example.com 也收不到

Public Suffix List(PSL)保护:浏览器内置公共后缀列表,Domain=comDomain=github.io 等公共后缀一律拒绝,防止跨站污染。

重要区分:同源 ≠ 同站

同源(Same-Origin):协议 + host + 端口 完全相同
同站(Same-Site)  :注册域(eTLD+1)相同即可app.example.com  vs  api.example.com→ 不同源(host 不同)→ 需要 CORS→ 同站(同属 example.com)→ SameSite=Lax Cookie 可以发送
SameSite 值 同站 fetch 跨站顶层跳转 跨站 fetch/XHR 典型用途
Strict 高安全敏感操作
Lax(默认) 通用场景
None; Secure 跨站嵌入(受第三方 Cookie 封锁影响)

当 SPA(app.example.com)用 fetch 请求认证服务器(auth.okta.com)时,认证服务器尝试写入的 Cookie 属于第三方 Cookie

  • Safari(ITP):默认封锁
  • Chrome(Privacy Sandbox):逐步封锁
  • Firefox:默认封锁

这一趋势直接影响了依赖第三方 Cookie 的 OAuth 静默刷新方案。


四、OAuth 2.0 在 SPA 中的实践

4.1 核心概念回顾

Token 类型:

Token 有效期 作用
Access Token 短(5~15 分钟) 携带在请求头,证明访问权限
Refresh Token 长(天/周级别) 用于换取新的 Access Token

客户端类型:

类型 是否能保密 典型场景 换 Token 方式
Public Client ❌ 代码对用户可见 浏览器 SPA、移动 App PKCE,无 client_secret
Confidential Client ✅ 代码在服务器 有后端的 Web 应用 client_id + client_secret

client_secret 为何不能放在 SPA:

SPA 代码运行在浏览器→ 任何人打开 DevTools → 网络面板看请求参数→ 源码面板看 JS 代码→ client_secret 形同公开,毫无意义

PKCE(Proof Key for Code Exchange)是公开客户端的替代方案:每次登录动态生成一次性随机数,即使 code 被截获,没有对应的 verifier 也无法换取 Token。

4.2 静默刷新的历史与淘汰

早期 OAuth 2.0 的 Implicit Flow 专为 SPA 设计,但不发放 Refresh Token(认为浏览器存 Refresh Token 不安全)。Access Token 过期后,只能靠隐藏 iframe 续命:

主页面└── 创建隐藏 <iframe>src = 认证服务器 /authorize?prompt=none↓认证服务器读取 SSO Session Cookie(第一方 Cookie,登录时种下)↓┌─ Cookie 有效 → 直接返回新 Token 到 iframe URL hash└─ Cookie 无效 → 返回 login_required 错误↓iframe JS 读取 hash → window.parent.postMessage({ token })↓
主页面 message 事件 → 更新 Access Token → 销毁 iframe

该方案被淘汰的原因:

依赖 iframe 中的第三方 Cookie(SSO Session)↓
Safari ITP / Chrome Privacy Sandbox 封锁第三方 Cookie↓
iframe 无法携带认证服务器的 Session Cookie↓
prompt=none 永远返回 login_required↓
静默刷新失效,用户被迫重新登录

现在的推荐:Authorization Code Flow + PKCE,配合 Refresh Token 实现续签。


五、SPA + Web API 的两种认证模式

实际项目中,SPA 通常有自己的 Web API,两者的 部署 关系直接影响架构选择。

5.1 跨源请求的性质分类

请求 是否受 CORS 限制 原因
浏览器重定向到认证服务器登录页 页面跳转,非 fetch
SPA fetch 认证服务器 /token 跨源 API 调用
服务端调认证服务器 /token 服务端对服务端,无浏览器参与
SPA 调自家 Web API 取决于部署 同域无问题,跨域需配 CORS

5.2 模式一:SPA 作为 OAuth 客户端

SPA 直接完成 OAuth 流程,持有 Token。

Web API认证服务器浏览器 SPAWeb API认证服务器浏览器 SPA① 重定向登录(带 PKCE code_challenge)② 重定向回 SPA(带 authorization code)③ POST /token(带 code + code_verifier)④ 返回 Access Token + Refresh Token⑤ 请求(Bearer Access Token)⑥ 验证 JWT 签名,返回数据⑦ Access Token 过期,用 Refresh Token 换新

Token 归属:

  • Access Token → SPA 内存(页面关闭即丢失)
  • Refresh Token → SPA 内存或 HttpOnly Cookie(有跨站限制)

适用场景: 快速开发、无敏感数据、无 client_secret 需求的中小项目。

风险: Refresh Token 在浏览器侧,XSS 攻击可能窃取。

5.3 模式二:Web API 作为 OAuth 客户端(推荐)

既然项目已有 Web API,让它承担 OAuth 客户端角色,Token 完全不下发到浏览器。

Token 归属:

浏览器侧             │         服务端(Web API)│
Session Cookie ──────┼──→ sessions 表
(不透明 ID)         │    ┌─────────────────────────────────┐│    │ sid │ user_id │ access_token │ refresh_token ││    └─────────────────────────────────┘│         ↑│    Token 永远不离开服务端

SPA 的 Session Cookie 需要 CORS 吗?

取决于部署方式:

情况一:反向代理同源(推荐)Nginx 统一 example.com/      → SPA 静态文件/api/  → Web API→ 完全同源,无 CORS,无 SameSite 问题情况二:子域分离app.example.com(SPA)api.example.com(Web API)→ 不同源,但同站(SameSite=Lax 生效)→ Web API 配置 CORS 允许 app.example.com→ Session Cookie 设置 Domain=example.com→ SameSite=Lax,同站 fetch 可以携带 ✅

Access Token 和 Refresh Token 在模式二中是否多余?

不多余,只是对 SPA 透明:

场景 Token 的用途
有下游微服务 / 第三方 API Web API 用 Access Token 调用下游
纯单体 Web API Token 用于初次身份确认(/userinfo)和 SSO 登出(/revoke)
多系统 SSO 统一认证服务器识别同一用户

5.4 两种模式对比

模式一(SPA 持 Token) 模式二(后端持 Token)
Token 存放 浏览器内存 服务端数据库
XSS 风险 ⚠️ Token 有被窃风险 ✅ Token 不进浏览器
支持 client_secret
CORS 复杂度 SPA 需直接跨域调认证服务器 服务端调,无 CORS
第三方 Cookie 影响 ⚠️ 静默刷新依赖受限 ✅ 不依赖第三方 Cookie
架构复杂度
OAuth 社区推荐 ⚠️ 可用,但需谨慎 ✅ RFC 9700 推荐

六、综合推荐架构

                     ┌─────────────────────────────────┐│         认证服务器                ││   (Auth0 / Keycloak / 自建)       │└──────────────┬──────────────────┘│ 服务端对服务端│ 无 CORS,可用 client_secret┌──────────────▼──────────────────┐│         Web API 后端              ││  · 持有 Access Token(内存/Redis)││  · 持有 Refresh Token(数据库)   ││  · 签发 Session(HttpOnly Cookie)│└──────────────┬──────────────────┘│ 同源或同站│ Session Cookie(SameSite=Lax)┌──────────────▼──────────────────┐│            SPA                   ││  · 只持有 Session Cookie          ││  · 从不接触 Token 本体            │└─────────────────────────────────┘

部署层面,Nginx 反向代理同源化是最简洁的选择:

server {listen 443 ssl;server_name example.com;location / {root /var/www/spa;           # SPA 静态文件,同源}location /api/ {proxy_pass http://web-api:8080/;  # 反向代理,浏览器视为同源proxy_set_header Host $host;}
}

这一设置同时消灭了 CORS 问题和 SameSite 限制,是大多数项目的最优起点。


七、总结

问题 关键结论
什么是 SOP 浏览器限制 JS 只能读取同源响应,防止跨站数据窃取
什么是 CORS 服务器声明允许哪些外部源读取响应;是浏览器机制,非浏览器环境无效
预检能否跳过 不能;可通过 Max-Age 缓存或反向代理同源化规避
Cookie Domain 规则 响应 host 必须是 Domain 的子域或本身;不带 Domain 则 Host-Only
SameSite 的核心 同站(eTLD+1 相同)≠ 同源;SameSite=Lax 允许同站 fetch
静默刷新为何淘汰 依赖第三方 Cookie,被现代浏览器逐步封锁
SPA 该用哪种模式 有 Web API 优先选模式二,Token 全部留在服务端,浏览器只持 Session Cookie
最优部署方式 Nginx 反向代理同源化,同时解决 CORS 和 Cookie 跨站问题

浏览器的安全边界是由浏览器划定的,服务器只能在规则内表达意图;理解这一点,是设计健壮 Web 认证架构的前提。

http://www.jsqmd.com/news/901981/

相关文章:

  • Laravel项目构建语义搜索引擎:从向量化到混合搜索实战
  • JetBrains IDE试用期重置终极指南:3分钟恢复30天免费试用
  • 魔兽争霸III终极增强指南:用WarcraftHelper重燃经典游戏体验
  • 从无脑转发到精准投喂:GPT-4高效协作的提示工程与工作流设计
  • MCB2100评估板CAN通信故障排查与解决方案
  • Rails AI应用后台任务实战:Active Job异步处理与队列选型
  • 面向 GitHub 协作的 Git 实战规范:分支、PR、Actions 与常见事故处理
  • 基于FastAPI、Groq与Streamlit构建语音交互AI智能体全栈实践
  • 突破自动化瓶颈:构建AI驱动的n8n工作流管道架构
  • 2026年榆林市黄金回收门店权威推荐榜单 彩金+铂金+金条+白银回收门店口碑精选+联系方式 - 大熊猫898989
  • 从ScrollView到高性能列表:CocosCreator中drawcall合并与对象池的保姆级配置流程
  • 2026年4月市面上靠谱的景观棚公司推荐,充电桩棚/膜结构车棚/停车棚/伸缩篷/景观棚/电动推拉棚,景观棚定制厂家哪个好 - 品牌推荐师
  • 艾尔登法环帧率解锁与优化终极指南:告别60帧限制,开启流畅体验
  • 从‘见光死’到均匀出光:用Ansys Speos Light Guide解决汽车内饰灯条亮度不均的实战案例
  • 2026广东靠谱全屋定制品牌评测指南 - 服务品牌热点
  • 2026年牵手红娘服务权威推荐深度解析:婚恋场景匹配效率低与信任成本高 - 品牌推荐
  • CAD依赖管理:挑战、解决方案与工业实践
  • 别再只用isNumeric了!Java字符串数字校验的5个真实业务场景与避坑指南
  • 大语言模型幻觉应对指南:从原理到实战的防“胡说八道”策略
  • Python颠覆视频剪辑:JianYingApi如何实现剪映的终极自动化革命?
  • 面向AI Agent的API设计:从人类中心到智能体优先的范式转变
  • 2026年咸阳市黄金回收门店权威推荐榜单 彩金+铂金+金条+白银回收门店口碑精选+联系方式 - 大熊猫898989
  • 2026年玉林市黄金回收门店权威推荐榜单 彩金+铂金+金条+白银回收门店口碑精选+联系方式 - 大熊猫898989
  • 2026年玉溪市黄金回收门店权威推荐榜单 彩金+铂金+金条+白银回收门店口碑精选+联系方式 - 大熊猫898989
  • 2026年曲靖市黄金回收门店权威推荐榜单 彩金+铂金+金条+白银回收门店口碑精选+联系方式 - 大熊猫898989
  • AI应用成本管理实战:TokenBar如何实现LLM开销透明化与优化
  • 别再只把UMAP当可视化工具了:用Python实战MNIST手写数字分类,解锁降维的隐藏用法
  • Wireshark实战:拆解一个网页加载背后的所有HTTP请求(含长文档与图片)
  • 面试官问‘CPU怎么算1+1’?从晶体管到超前进位,一次讲清加法器的底层逻辑与优化演进
  • 2026年湘潭市黄金回收门店权威推荐榜单 彩金+铂金+金条+白银回收门店口碑精选+联系方式 - 大熊猫898989