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

浏览器的同源策略(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") { ... })。
相关推荐
安卓开发者4 小时前
鸿蒙NEXT系统Picker全解析:安全高效的用户资源访问之道
安全·华为·harmonyos
安卓开发者4 小时前
鸿蒙NEXT安全控件解析:实现精准权限管控的新范式
安全·华为·harmonyos
wanhengidc9 小时前
BGP高防服务器具体是指什么
运维·服务器·网络·安全·游戏·智能手机
hello_25011 小时前
k8s安全机制解析:RBAC、Service Account与安全上下文
java·安全·kubernetes
汽车仪器仪表相关领域11 小时前
工业安全新利器:NHQT-4四合一检测线系统深度解析
网络·数据库·人工智能·安全·汽车·检测站·汽车检测
鼓掌MVP12 小时前
Lighthouse安全组自动化审计与加固:基于MCP协议的智能运维实践
运维·安全·自动化·腾讯轻量云ai创想家
lypzcgf12 小时前
Coze源码分析-资源库-创建数据库-后端源码-安全与错误处理
数据库·安全·go·coze·coze源码分析·ai应用平台·agent平台
AndyYang201712 小时前
nmap 基本扫描命令
服务器·网络·安全·渗透测试·nmap·扫描工具
ISACA中国14 小时前
《第四届数字信任大会》精彩观点:腾讯经验-人工智能安全风险之应对与实践|从十大风险到企业级防护架构
人工智能·安全·架构·漏洞案例·大模型越狱·企业级防护