浏览器同源策略与跨域解决方案

浏览器的同源策略(Same-Origin Policy) 是 Web 安全的核心基石,其本质是限制不同源的页面对当前页面资源的 "非法访问",避免恶意网站窃取数据、篡改 DOM 或发起未授权请求。理解同源策略及跨域解决方案,是前端开发中处理多服务协作的关键。

一、什么是同源策略?

"同源" 指的是两个页面的协议(Protocol)、域名(Domain)、端口(Port) 三者完全一致。只要其中任意一项不同,就属于 "不同源",会被浏览器的同源策略拦截。

1. 同源判定规则

判断两个 URL 是否同源,需严格比对以下三点:

对比维度 示例 1(当前页面) 示例 2(目标资源) 是否同源 原因
协议 + 域名 + 端口 http://www.example.com:80 http://www.example.com:80/index.html 三者完全一致
协议不同 http://www.example.com https://www.example.com 协议从 HTTP 变为 HTTPS
域名不同 http://www.example.com http://blog.example.com 子域名不同(www vs blog)
端口不同 http://www.example.com:80 http://www.example.com:8080 端口从 80 变为 8080

2. 同源策略的限制范围

同源策略并非 "一刀切",而是对不同类型的资源访问做了差异化限制,核心限制包括:

  • 1. 脚本访问限制(最核心) 不同源的 JavaScript 脚本,无法直接操作当前页面的 DOM(如修改document内容)、读取 Cookie/LocalStorage/SessionStorage,也无法通过XMLHttpRequestFetch API发起请求(会触发跨域拦截)。例:http://a.com的脚本无法读取http://b.com的 LocalStorage,也无法直接请求http://b.com/api/data

  • 2. 资源加载限制(部分允许) 部分 "被动加载" 的资源不受同源限制,例如:

    • 图片(<img src="不同源URL">):可加载但无法读取像素数据(避免窃取图片内容);
    • 样式表(<link href="不同源URL">):可加载但无法通过getComputedStyle读取样式(避免信息泄露);
    • 脚本(<script src="不同源URL">):可加载执行(即 "JSONP" 的原理),但无法访问脚本的内部变量。
  • 3. Cookie 访问限制 Cookie 的 "源" 判定依赖域名 (不包含端口和协议),不同源的页面无法读取对方的 Cookie;但如果 Cookie 设置了Domain属性(如Domain=example.com),则www.example.comblog.example.com等子域名可共享该 Cookie(仍受同源策略的 "域名匹配" 约束)。

二、为什么需要跨域?

同源策略保障了安全,但随着 Web 应用的架构演进(如前后端分离、微服务),"跨域访问" 成为刚需:

  • 前后端分离:前端部署在http://frontend.com,后端 API 部署在http://backend.com,前端需跨域请求后端数据;
  • 微服务协作:A 服务(http://service-a.com)需调用 B 服务(http://service-b.com)的接口获取数据;
  • 第三方资源引用:如引用https://cdn.example.com的图片、脚本,或调用微信、支付宝的开放 API。

此时,需要在 "安全" 和 "可用性" 之间找到平衡,通过技术方案实现合法的跨域访问

三、常见的跨域解决方案

跨域解决方案的核心思路是:在浏览器的同源策略规则内,通过 "协议约定" 或 "中间层转发",让不同源的资源访问被浏览器认可。以下是前端和后端常用的 4 种方案,按 "通用性" 和 "安全性" 排序:

1. CORS(Cross-Origin Resource Sharing):最推荐的方案

CORS 是 W3C 标准定义的跨域解决方案,本质是后端通过 HTTP 响应头,明确告知浏览器 "允许哪个源的请求访问" ,浏览器验证通过后放行请求。CORS 分为 "简单请求" 和 "预检请求(Preflight)" 两类,覆盖绝大多数跨域场景。

(1)简单请求(无需预检)

满足以下条件的请求,浏览器直接发送,无需额外验证:

  • 请求方法为:GETPOSTHEAD
  • 请求头仅包含:AcceptAccept-LanguageContent-LanguageContent-Type(且值为application/x-www-form-urlencodedmultipart/form-datatext/plain)。

实现方式 :后端在响应头中添加Access-Control-Allow-Origin,示例:

js 复制代码
# 后端响应头(以Node.js Express为例)
res.setHeader("Access-Control-Allow-Origin", "http://frontend.com"); // 允许指定源跨域
res.setHeader("Access-Control-Allow-Credentials", "true"); // 允许携带Cookie(可选)
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // 允许的请求方法
res.setHeader("Access-Control-Allow-Headers", "Content-Type"); // 允许的请求头
  • Access-Control-Allow-Origin:核心字段,值可以是具体域名(如http://frontend.com)或*(允许所有源,但不能与Credentials=true同时使用 ,因为*会暴露 Cookie 安全风险);
  • Access-Control-Allow-Credentials:设为true时,前端请求需携带credentials: true(如fetchcredentials: 'include'),且Allow-Origin不能为*
  • Access-Control-Expose-Headers:默认情况下,前端只能读取响应头的Cache-ControlContent-Language等基础字段,通过该字段可指定额外允许读取的头(如X-Total-Count)。

(2)预检请求(Preflight)

不满足 "简单请求" 条件的请求(如PUT/DELETE方法、自定义请求头X-Token),浏览器会先发送一次OPTIONS请求(预检),询问后端 "是否允许该跨域请求",后端响应通过后,才发送真正的业务请求。

流程示例

  1. 前端发送OPTIONS预检请求,携带头信息:
http 复制代码
OPTIONS /api/data HTTP/1.1
Origin: http://frontend.com
Access-Control-Request-Method: PUT // 告知后端真实请求方法
Access-Control-Request-Headers: X-Token // 告知后端真实请求的自定义头
  1. 后端响应预检请求,确认允许:
http 复制代码
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的所有方法
Access-Control-Allow-Headers: X-Token // 允许的自定义头
Access-Control-Max-Age: 86400 // 预检结果缓存时间(24小时,避免重复预检)

优势与适用场景

  • 优势:标准方案、支持所有 HTTP 方法、安全性高(后端精确控制允许的源);
  • 适用场景:前后端分离项目、微服务间 API 调用(几乎所有跨域场景的首选)。

2. 代理服务器(Proxy):前端无感知的方案

代理服务器的核心思路是:前端不直接请求跨域的后端 API,而是请求 "同源的代理服务器",由代理服务器转发请求到后端 API,再将响应返回给前端。由于前端与代理服务器 "同源",不存在跨域问题;代理服务器是后端服务,不受浏览器同源策略限制。

实现方式(以开发环境和生产环境为例)

  • 开发环境(Webpack/Vite 代理) :前端工程化工具(如 Webpack、Vite)内置代理功能,只需配置代理规则即可。示例(Vite 配置vite.config.js):
js 复制代码
export default defineConfig({
  server: {
    proxy: {
      // 匹配以/api开头的请求,转发到后端API
      '/api': {
        target: 'http://backend.com', // 后端API地址
        changeOrigin: true, // 转发时修改请求的Origin为target(避免后端验证Origin失败)
        rewrite: (path) => path.replace(/^/api/, '') // 可选:去掉/api前缀(如前端请求/api/data,实际转发到http://backend.com/data)
      }
    }
  }
});

配置后,前端只需请求/api/data(同源路径),Vite 会自动转发到http://backend.com/data

  • 生产环境(Nginx 代理) :生产环境通常用 Nginx 作为静态资源服务器和代理服务器,配置示例:
nginx 复制代码
server {
  listen 80;
  server_name frontend.com; # 前端域名(同源)

  # 静态资源(前端页面)
  location / {
    root /usr/share/nginx/html;
    index index.html;
  }

  # 代理跨域请求到后端API
  location /api {
    proxy_pass http://backend.com; # 后端API地址
    proxy_set_header Host $host; # 传递请求头Host
    proxy_set_header X-Real-IP $remote_addr; # 传递真实IP
  }
}

此时,前端请求http://frontend.com/api/data,Nginx 会转发到http://backend.com/data,实现跨域。

优势与适用场景

  • 优势:前端代码无需修改(无感知跨域)、支持所有请求方法、安全性高(代理服务器可做权限校验);
  • 适用场景:开发环境快速调试、生产环境前后端分离项目(尤其适合后端无法修改 CORS 配置的场景)。

3. JSONP:古老但仍有用的方案

JSONP(JSON with Padding)是利用 **<script>标签不受同源策略限制 ** 的特性实现跨域的方案,本质是 "后端返回一段可执行的 JavaScript 代码,前端通过回调函数获取数据"。

实现流程

  1. 前端定义回调函数handleData,并通过<script>标签请求后端接口(携带回调函数名):
js 复制代码
// 1. 定义回调函数
function handleData(data) {
  console.log("跨域获取的数据:", data); // 处理后端返回的数据
}

// 2. 创建<script>标签,请求后端接口(携带回调函数名callback=handleData)
const script = document.createElement("script");
script.src = "http://backend.com/api/data?callback=handleData"; // 后端需解析callback参数
document.body.appendChild(script);

// 3. 加载完成后移除<script>标签(可选,避免冗余)
script.onload = () => {
  document.body.removeChild(script);
};
js 复制代码
// 后端响应(以Node.js为例)
const callback = req.query.callback; // 获取前端传递的回调函数名
const data = { name: "张三", age: 20 }; // 要返回的数据
res.send(`${callback}(${JSON.stringify(data)})`); // 返回:handleData({"name":"张三","age":20})

优势与局限性

  • 优势:兼容性好(支持所有浏览器,包括 IE)、实现简单;
  • 局限性:仅支持GET请求(<script>标签只能发起 GET)、安全性低(后端返回的代码可能被注入恶意脚本,需严格校验数据)。
  • 适用场景:仅用于兼容老旧浏览器,或获取第三方公开资源(如百度地图 API 的 JSONP 接口)

4. WebSocket:全双工通信的跨域方案

WebSocket 是 HTML5 定义的全双工通信协议,其握手阶段基于 HTTP,但一旦建立连接,后续数据传输不受同源策略限制(因为 WebSocket 协议本身不依赖 "源" 的概念)。

实现方式

  1. 前端创建 WebSocket 连接(URL 以ws://wss://开头)
js 复制代码
const ws = new WebSocket("ws://backend.com/ws"); // 跨域连接

// 连接成功
ws.onopen = () => {
  ws.send("前端发送的消息"); // 发送数据
};

// 接收后端消息
ws.onmessage = (event) => {
  console.log("后端返回:", event.data);
};

// 连接关闭
ws.onclose = () => {
  console.log("连接关闭");
};
  1. 后端搭建 WebSocket 服务(以 Node.js 的ws库为例):
js 复制代码
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });

// 监听连接
wss.on("connection", (ws) => {
  // 接收前端消息
  ws.on("message", (message) => {
    console.log("前端消息:", message.toString());
    // 发送消息给前端
    ws.send("后端收到消息:" + message.toString());
  });
});

优势与适用场景

  • 优势:全双工通信(前后端可实时双向发送数据)、不受同源策略限制;
  • 适用场景:实时通信场景(如聊天应用、实时数据看板、WebSocket API 调用)。

五、跨域相关的安全风险

跨域方案在解决 "可用性" 的同时,需警惕安全风险:

  1. CORS 配置不当 :若Access-Control-Allow-Origin设为*且允许Credentials=true,会导致 Cookie 被任意网站窃取;应尽量指定具体的允许源(如http://frontend.com)。
  2. JSONP 注入 :后端未校验callback参数,可能被注入恶意代码(如callback=恶意函数);需对callback参数做白名单校验(仅允许字母、数字、下划线)。
  3. postMessage 滥用 :接收方未验证event.origin(消息来源),可能接收恶意网站的消息;应在message事件中判断event.origin是否为可信源(如if (event.origin === "http://trusted.com") { ... })。
相关推荐
用户962377954485 小时前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机9 小时前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机9 小时前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户9623779544810 小时前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star10 小时前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户9623779544814 小时前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
Wect2 天前
浏览器缓存机制
前端·面试·浏览器
cipher2 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行5 天前
网络安全总结
安全·web安全
red1giant_star5 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全