news 2026/5/28 11:37:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
浏览器安全机制与现代 SPA 认证架构深度解析

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

1.1 什么是同源

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

URLhttps://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:存储与发送规则

3.1 浏览器保存 Cookie 的 Domain 规则

浏览器收到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等公共后缀一律拒绝,防止跨站污染。

3.2 SameSite:控制 Cookie 的发送时机

重要区分:同源 ≠ 同站

同源(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 封锁影响)

3.3 第三方 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、移动 AppPKCE,无 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 完全不下发到浏览器。

认证服务器Web API(OAuth 客户端)浏览器 SPA认证服务器Web API(OAuth 客户端)浏览器 SPA① 重定向登录(回调地址指向 Web API)② 重定向到 Web API /callback(带 code)③ POST /token(带 code + client_secret)④ 返回 Access Token + Refresh Token⑤ Refresh Token 存数据库,建立 Session⑥ Set-Cookie: sid=xxx(HttpOnly,SameSite=Strict)⑦ 后续请求携带 Session Cookie⑧ 验证 Session,返回业务数据

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 的用途
有下游微服务 / 第三方 APIWeb API 用 Access Token 调用下游
纯单体 Web APIToken 用于初次身份确认(/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 认证架构的前提。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 11:36:18

免费AMD Ryzen调试工具SMUDebugTool:从入门到精通的完整指南

免费AMD Ryzen调试工具SMUDebugTool&#xff1a;从入门到精通的完整指南 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: http…

作者头像 李华
网站建设 2026/5/28 11:33:26

FFmpegGUI:重新定义视频处理工作流的跨平台图形界面工具

FFmpegGUI&#xff1a;重新定义视频处理工作流的跨平台图形界面工具 【免费下载链接】ffmpegGUI ffmpeg GUI 项目地址: https://gitcode.com/gh_mirrors/ff/ffmpegGUI 核心理念与差异化优势 FFmpegGUI是一款基于现代Web技术栈构建的开源图形界面工具&#xff0c;旨在彻…

作者头像 李华
网站建设 2026/5/28 11:31:13

MySQL 存储过程与触发器完全指南

引言在前面的 MySQL 文章中&#xff0c;我们学习了 SQL 的基础操作、事务、索引、视图和多表查询。这些已经能覆盖大部分 CRUD 需求。但在实际项目中&#xff0c;还有两类重要的数据库编程技术需要掌握&#xff1a;存储过程&#xff1a;把一组 SQL 语句封装成可重复调用的"…

作者头像 李华
网站建设 2026/5/28 11:29:40

从手工画线到智能分析:3分钟掌握缠论量化的终极可视化工具

从手工画线到智能分析&#xff1a;3分钟掌握缠论量化的终极可视化工具 【免费下载链接】chanvis 基于TradingView本地SDK的可视化前后端代码&#xff0c;适用于缠论量化研究&#xff0c;和其他的基于几何交易的量化研究。 缠论量化 摩尔缠论 缠论可视化 TradingView TV-SDK 项…

作者头像 李华