Skip to content

同源策略

概念

同源策略是一个重要的安全策略,用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。

同源策略的目的是防止恶意网站窃取用户在其他网站上的敏感信息或进行未经授权的操作,保护用户数据的安全性和隐私性

如果两个 URL 的协议、主机和端口都相同,就称这两个 URL 同源。

  • 协议:是定义了数据如何在计算机内和之间进行交换的规则的系统,如 HTTP,HTTPS

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备,网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议簇参与的网络的计算机也可称为 IP 主机

  • 端口:主机是计算机到计算机之间的通信,端口是进程和进程之间的通信

同源策略主要表现在三个方面:DOM、Web 数据和网络

  • DOM 访问限制:同源策略限制了网页脚本访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息

  • web 数据限制:同源策略也限制了从其他源加载的 web 数据。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。有助于防止跨站点请求伪造 CSRF 等攻击

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为

解决方案

代理服务器:位于发起请求的客户端与原始服务器端之间的一台跳板服务器

正向代理可以隐藏客户端,反向代理可以隐藏原始服务器

● 正向代理:客户端<-->代理-->服务端,位于客户端和原始服务器之间的服务器

● 反向代理:客户端-->代理<-->服务端,对于客户端而言它就像原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理命名空间中的内容发送普通请求,接着反向代理将判断向何处转交请求,并将获得的内容返回给客户端,就像这些内容原本就是他自己的一样

跨域:cross-origin 是指在网络中,一个源的网页或者网站尝试去访问另一个不同源的资源时发生的情况。“源”:协议、域名、端口号

● 代理:

● CORS:后端配置

● jsonp 前后端协商:

原理:script 标签的 src 不受同源策略的限制,但只能使用 get 请求

CORS跨域资源共享

CORS(Cross-Origin Resource Sharing)是一种服务器解决跨域的标准方案,它通过设置HTTP头允许特定源访问服务器资源。

CORS是目前最推荐的解决方案,因为它符合安全标准并且支持所有主流浏览器

服务器端设置CORS响应头

  • Access-Control-Allow-Origin:允许的跨域源,例如http://example.com*(允许所有源)

  • Access-Control-Allow-Methods:允许的HTTP方法,例如GET,POST,PUT,DELETE

  • Access-Control-Allow-Headers:允许的请求头信息,例如Content-TypeAuthorization

  • Access-Control-Allow-Credentials:是否允许携带凭证(如cookie),设置为true时,Access-Control-Allow-Origin不能为*

前端请求示例

前端不需要额外的配置,只需确保服务器支持CORS响应头即可

nginx基本跨域配置

server {
    listen 80;
    server_name api.example.com;

    location / {
        # 设置允许跨域的源
        add_header 'Access-Control-Allow-Origin' 'http://example.com';

        # 设置允许的方法
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';

        # 设置允许的头部信息
        add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization';

        # 是否允许携带凭证(如 Cookie)
        add_header 'Access-Control-Allow-Credentials' 'true';

        # 如果请求方法是 OPTIONS,则直接返回
        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://localhost:5000; # 转发到后端服务
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

允许所有源跨域(仅在开发环境使用)

在开发环境中,可能希望允许所有源访问接口。可以将 Access-Control-Allow-Origin 配置为 *

add_header 'Access-Control-Allow-Origin' '*';

条件性跨域(允许多个特定源)

可以通过map指令实现动态设置Access-Control-Allow-Origin

map $http_origin $cors_origin {
    default "";
    "http://example.com" $http_origin;
    "http://another-example.com" $http_origin;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        add_header 'Access-Control-Allow-Origin' $cors_origin;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
        add_header 'Access-Control-Allow-Credentials' 'true';

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://localhost:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

全局配置跨域

如果多个服务器块都需要相同的CORS配置,可以将跨域配置放在http块中,使之成为全局配置

fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include'  // 如果需要携带凭证(如 Cookie)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

JSONP(JSON with Padding)

JSONP 是一种传统的跨域方案,利用 <script> 标签没有跨域限制的特点。通过将请求的 URL 写在 <script> 标签的 src 属性上,来实现跨域请求。服务器返回一个 JSON 格式的回调函数。JSONP 只能用于 GET 请求。

实现

服务器返回一个回调函数调用

js
// 服务器返回的数据
callback({"name": "John", "age": 30});

前端代码

js
function jsonpRequest(url, callback) {
  const script = document.createElement('script');
  script.src = `${url}?callback=${callback.name}`;
  document.body.appendChild(script);
  document.body.removeChild(script);
}

// 使用 JSONP 请求
jsonpRequest('https://api.example.com/data', function(data) {
  console.log(data);
});

优点:简单,不需要服务器端额外配置

缺点:只能用于GET请求,且安全性较差,不推荐在现代项目中使用

代理服务器

通过在服务器端设置一个代理,把跨域请求转发到目标服务器,前端向代理服务器发送请求,由代理服务器转发到真正的目标服务器,并将响应返回给前端

使用NGINX代理

server {
    listen 80;
    server_name yourdomain.com;

    location /api/ {
        proxy_pass http://api.example.com/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

优点:支持 GET、POST 等所有 HTTP 方法,且安全可靠。

缺点:需要额外的代理服务器配置,适合在生产环境中使用。

webpack dev server代理

在开发环境中,如果使用 Webpack,可以通过 Webpack Dev Server 配置代理来解决跨域问题。

在 webpack.config.js 中添加以下配置:

js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.example.com', // 目标服务器
        changeOrigin: true,              // 修改源头
        pathRewrite: { '^/api': '' }     // 去掉路径中的 /api
      }
    }
  }
};

vite配置

在 vite.config.js 中配置代理

js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://backend-server.com', // 代理目标地址
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径
      },
    },
  },
});

优点:方便开发,不需要后端额外配置。

缺点:只适用于本地开发环境,不适合生产环境。

同源的iframe+postMessage

可以在同源服务器上加载一个 iframe,通过 postMessage 与 iframe 通信来绕过跨域限制。

假设有两个应用,一个在 http://example.com,另一个在 http://api.example.com

设置 iframe

1、在 http://api.example.com 上创建一个 HTML 文件,用于接收消息并发起请求:

html
<!-- http://api.example.com/proxy.html -->
<script>
  window.addEventListener('message', function(event) {
    if (event.origin !== 'http://example.com') return;

    fetch('/data')
      .then(response => response.json())
      .then(data => event.source.postMessage(data, event.origin))
      .catch(error => console.error('Error:', error));
  });
</script>

2、在前端应用 http://example.com 上加载 iframe 并使用 postMessage 通信:

js
const iframe = document.createElement('iframe');
iframe.src = 'http://api.example.com/proxy.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);

iframe.onload = () => {
  iframe.contentWindow.postMessage('fetchData', 'http://api.example.com');
};

window.addEventListener('message', function(event) {
  if (event.origin === 'http://api.example.com') {
    console.log('Received data:', event.data);
  }
});

优点:可以支持任意请求,适合跨域访问。

缺点:实现复杂,难以维护,推荐在特殊场景中使用。

使用document.domain(仅限同一主域的子域之间)

对于同一个主域的不同子域,可以通过将 document.domain 设置为同一主域来解决跨域问题。

假设两个子域分别是 a.example.com 和 b.example.com,可以通过以下方式实现:

在 a.example.com 和 b.example.com 的页面上都设置:

js
document.domain = 'example.com';

此时,两个子域就可以共享 document 对象中的数据。

优点:简单方便,适用于同一主域的不同子域。

缺点:只能用于相同主域的子域之间,无法解决不同主域的跨域问题。

浏览器有同源策略,cdn请求资源为何没有?

一些CDN资源不受同源策略的限制,是因为同源策略主要是浏览器的安全机制,用于限制不同源的文档或脚本之间的交互操作,但对于某些特定类型的资源访问,浏览器会有一些例外操作

在HTML中,<script>,<img>,<iframe>,<link>等标签的src属性指向的资源通常是可以跨域访问的,例如,可以通过<script src="https://cdn.example.com/vue.min.js">加载cdn资源

这样做的主要原始是为了保证web的开放性和可扩展性,如果这些资源也严格受到同源策略的限制,那么所有相关资源都部署在同一个服务器下会未被Web开放的初衷,并且不利于资源的分发和缓存

注意:然后浏览器允许这些资源的跨域加载,但在加载JavaScript时,会限制对返回内容的读写权限,以防止恶意脚本获取或修改其他域的信息

CDN服务提供商通常也会采取一些措施来确保资源的安全性和合法性,会对资源进行管理和配置,只允许合法的请求访问资源,并防止恶意使用或滥用CDN资源

参考

https://juejin.cn/post/7269952188927017015

https://www.xiaohongshu.com/explore/66fbe39a000000002a031502?xsec_token=AB-0p6_AeeTrndOl6kL7rtktgNCSUO9SqDJi5JX_ae9xM=&xsec_source=pc_user

https://juejin.cn/post/7274544460104482873