JS `Same-Origin Policy` (同源策略) 的细致边界与规避方法

大家好,今天咱们来聊聊前端开发中一个老生常谈,但又经常让人头疼的问题:同源策略(Same-Origin Policy,简称 SOP)。这玩意儿就像前端世界的防火墙,保护着咱们的数据安全。但是吧,有时候又像个绊脚石,卡住咱们的开发流程。所以,彻底搞懂它,对每个前端工程师来说都是必修课。

废话不多说,咱们开始今天的“同源策略大讲堂”。

一、什么是“同源”?这哥仨咋就这么重要?

同源策略的核心在于“同源”这两个字。那什么是同源呢?浏览器会检查三个要素:

  1. 协议 (protocol): 比如 http 或者 https
  2. 域名 (host): 比如 example.com
  3. 端口 (port): 比如 80 或者 443

如果这三个要素都完全一样,那我们就说两个 URL 是同源的。只要有一个不一样,那就不算同源。

举个例子:

URL 同源吗? 理由
http://www.example.com/app1/index.html 同源 http://www.example.com/app2/index.html
https://www.example.com/index.html 不同源 协议不同 (http vs https)
http://api.example.com/index.html 不同源 域名不同 (www.example.com vs api.example.com)
http://www.example.com:8080/index.html 不同源 端口不同 (80 vs 8080)
http://www.example.com/index.html 不同源 http://www.example.com (没有明确指定index.html,算不同的路径,严格意义上是不同源,实际浏览器可能会有兼容性处理)

重要性:

同源策略是浏览器为了防止恶意网站窃取用户敏感信息而设置的安全机制。如果没有同源策略,恶意网站就可以轻易地通过 JavaScript 读取其他网站的数据,比如用户的 Cookie、localStorage 等,这想想都可怕。

二、同源策略的“边界”:哪些能做,哪些不能做?

同源策略主要限制以下几种行为:

  1. 跨域读取 Cookie、localStorage 和 IndexDB 数据: JavaScript 无法直接访问其他域下的 Cookie、localStorage 和 IndexDB 数据。

  2. 跨域 DOM 操作: JavaScript 无法直接修改其他域下的 DOM 元素。

  3. 跨域 AJAX 请求: JavaScript 无法直接发起跨域的 AJAX 请求。这是最常见的跨域问题,也是咱们今天要重点解决的。

但是,同源策略也有一些例外情况:

  • 跨域写操作 (Cross-origin writes) 是允许的: 比如,你可以通过 JavaScript 将数据提交到其他域的服务器。
  • 跨域嵌入 (Cross-origin embedding) 是允许的: 比如,你可以通过 <script src="..."> 引入其他域的 JavaScript 文件,或者通过 <img> 标签加载其他域的图片。
  • 某些跨域读操作 (Cross-origin reads) 是允许的: 比如,你可以通过 window.name 属性读取其他域的网页标题。

三、绕过同源策略的“十八般武艺”

既然同源策略这么严格,那咱们怎么才能在开发中绕过它呢?别慌,方法多的是,下面就给大家介绍几种常用的:

  1. JSONP (JSON with Padding)

    JSONP 是一种古老的跨域解决方案,它利用了 <script> 标签可以跨域加载资源的特性。

    原理:

    • 客户端创建一个 <script> 标签,并将请求的 URL 设置为服务器端的接口地址,同时指定一个回调函数名。
    • 服务器端接收到请求后,将数据包裹在回调函数中,并返回给客户端。
    • 客户端执行回调函数,从而获取数据。

    代码示例:

    • 客户端 (HTML):

      <!DOCTYPE html>
      <html>
      <head>
      <title>JSONP Example</title>
      </head>
      <body>
      <h1>JSONP Example</h1>
      <button onclick="getData()">Get Data</button>
      <script>
      function getData() {
      let script = document.createElement('script');
      script.src = 'http://api.example.com/data?callback=handleData'; // 替换成你的API地址
      document.body.appendChild(script);
      }
      
      function handleData(data) {
      console.log('Received data:', data);
      alert('Data received: ' + data.message); // 假设返回的数据格式是 { message: 'Hello' }
      }
      </script>
      </body>
      </html>
    • 服务器端 (Node.js 示例):

      const http = require('http');
      const url = require('url');
      
      const server = http.createServer((req, res) => {
      const parsedUrl = url.parse(req.url, true);
      const callback = parsedUrl.query.callback;
      
      if (req.url.startsWith('/data')) {
      const data = { message: 'Hello from server!' };
      const jsonpResponse = `${callback}(${JSON.stringify(data)})`;
      
      res.writeHead(200, { 'Content-Type': 'text/javascript' });
      res.end(jsonpResponse);
      } else {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('404 Not Found');
      }
      });
      
      server.listen(3000, () => {
      console.log('Server listening on port 3000');
      });

    缺点:

    • 只支持 GET 请求。
    • 安全性较低,容易受到 XSS 攻击。
  2. CORS (Cross-Origin Resource Sharing)

    CORS 是一种现代的跨域解决方案,它通过在服务器端设置 HTTP 响应头来告诉浏览器允许哪些跨域请求。

    原理:

    • 浏览器在发起跨域请求时,会自动添加一个 Origin 请求头,表明请求的来源。
    • 服务器端接收到请求后,会检查 Origin 请求头,如果允许该来源的请求,则在响应头中添加 Access-Control-Allow-Origin 字段,指定允许的来源。
    • 浏览器接收到响应后,会检查 Access-Control-Allow-Origin 字段,如果允许该来源的请求,则允许 JavaScript 读取响应数据。

    代码示例:

    • 服务器端 (Node.js 示例):

      const http = require('http');
      
      const server = http.createServer((req, res) => {
      // 允许所有来源的跨域请求
      res.setHeader('Access-Control-Allow-Origin', '*');
      
      // 允许特定的 HTTP 方法
      res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
      
      // 允许特定的请求头
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
      
      // 是否允许携带 Cookie
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      
      if (req.url === '/data') {
      const data = { message: 'Hello from server!' };
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));
      } else {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('404 Not Found');
      }
      });
      
      server.listen(3000, () => {
      console.log('Server listening on port 3000');
      });
    • 客户端 (JavaScript):

      fetch('http://api.example.com/data') // 替换成你的API地址
      .then(response => response.json())
      .then(data => {
      console.log('Received data:', data);
      alert('Data received: ' + data.message);
      })
      .catch(error => {
      console.error('Error:', error);
      });

    CORS 响应头解释:

    响应头 含义
    Access-Control-Allow-Origin 指定允许的来源。可以设置为 * (允许所有来源),也可以设置为具体的域名,比如 http://www.example.com。 *注意:在生产环境中,不要设置为 ``,除非你真的需要允许所有来源的跨域请求。**
    Access-Control-Allow-Methods 指定允许的 HTTP 方法。比如 GET, POST, PUT, DELETE
    Access-Control-Allow-Headers 指定允许的请求头。比如 Content-Type, Authorization
    Access-Control-Allow-Credentials 是否允许携带 Cookie。如果设置为 true,则客户端在发起请求时需要设置 credentials: 'include',否则 Cookie 将不会被发送。
    Access-Control-Max-Age 指定预检请求 (preflight request) 的缓存时间,单位为秒。预检请求是浏览器在发起跨域请求前,会先发送一个 OPTIONS 请求到服务器,询问服务器是否允许该跨域请求。如果服务器允许,则浏览器才会发起真正的请求。设置 Access-Control-Max-Age 可以减少预检请求的次数。
    Access-Control-Expose-Headers 指定允许客户端访问的响应头。默认情况下,客户端只能访问一些标准的响应头,比如 Content-TypeContent-Length 等。如果服务器端设置了自定义的响应头,并且希望客户端能够访问这些响应头,则需要在 Access-Control-Expose-Headers 中指定这些响应头的名称。

    CORS 的两种请求类型:

    • 简单请求 (Simple Request): 满足以下所有条件的请求被称为简单请求:

      • 请求方法是 GETHEADPOST
      • 请求头中只包含以下字段:
        • Accept
        • Accept-Language
        • Content-Language
        • Content-Type (只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain)
        • DPR
        • Downlink
        • ECT
        • RRT
        • Save-Data
        • Viewport-Width
        • Width
          简单请求不会触发预检请求。
    • 预检请求 (Preflight Request): 不满足简单请求条件的请求被称为预检请求。预检请求会先发送一个 OPTIONS 请求到服务器,询问服务器是否允许该跨域请求。

    优点:

    • 支持所有 HTTP 方法。
    • 安全性较高。

    缺点:

    • 需要服务器端配合设置响应头。
    • 某些老版本的浏览器可能不支持。
  3. 代理服务器 (Proxy Server)

    代理服务器是一种常用的跨域解决方案,它通过将客户端的请求发送到同域的服务器,再由同域的服务器转发到目标服务器,从而绕过同源策略。

    原理:

    • 客户端将请求发送到同域的代理服务器。
    • 代理服务器接收到请求后,将请求转发到目标服务器。
    • 目标服务器处理请求后,将响应返回给代理服务器。
    • 代理服务器将响应返回给客户端。

    代码示例:

    • 客户端 (JavaScript):

      fetch('/api/data') // 请求发送到同域的代理服务器
      .then(response => response.json())
      .then(data => {
      console.log('Received data:', data);
      alert('Data received: ' + data.message);
      })
      .catch(error => {
      console.error('Error:', error);
      });
    • 代理服务器 (Node.js 示例):

      const http = require('http');
      const httpProxy = require('http-proxy');
      
      const proxy = httpProxy.createProxyServer({});
      
      const server = http.createServer((req, res) => {
      proxy.web(req, res, {
      target: 'http://api.example.com' // 目标服务器地址
      });
      });
      
      server.listen(3000, () => {
      console.log('Proxy server listening on port 3000');
      });

    优点:

    • 客户端不需要做任何修改。
    • 可以绕过各种复杂的跨域限制。

    缺点:

    • 需要搭建和维护代理服务器。
    • 可能会增加服务器的负担。
  4. document.domain

    这种方法只适用于两个页面拥有相同的主域名,但是子域名不同。比如 a.example.comb.example.com

    原理:

    • 在两个页面中都设置 document.domain = 'example.com'
    • 这样浏览器就会认为这两个页面是同源的。

    代码示例:

    • a.example.com:

      <!DOCTYPE html>
      <html>
      <head>
      <title>a.example.com</title>
      <script>
      document.domain = 'example.com';
      </script>
      </head>
      <body>
      <h1>a.example.com</h1>
      <iframe src="http://b.example.com/iframe.html" id="myIframe"></iframe>
      <script>
      const iframe = document.getElementById('myIframe');
      iframe.onload = function() {
      // 尝试访问 iframe 中的内容
      try {
      console.log(iframe.contentWindow.document.body.innerHTML);
      } catch (e) {
      console.error('Error accessing iframe content:', e);
      }
      };
      </script>
      </body>
      </html>
    • b.example.com/iframe.html:

      <!DOCTYPE html>
      <html>
      <head>
      <title>b.example.com</title>
      <script>
      document.domain = 'example.com';
      </script>
      </head>
      <body>
      <h1>b.example.com</h1>
      <p>This is the iframe content.</p>
      </body>
      </html>

    缺点:

    • 安全性较低,容易受到 CSRF 攻击。
    • 只适用于特定场景。
  5. window.postMessage

    window.postMessage 是一种安全可靠的跨域通信机制,它允许不同源的页面之间进行消息传递。

    原理:

    • 在一个页面中使用 window.postMessage 方法向另一个页面发送消息。
    • 另一个页面监听 message 事件,接收消息。

    代码示例:

    • a.example.com:

      <!DOCTYPE html>
      <html>
      <head>
      <title>a.example.com</title>
      </head>
      <body>
      <h1>a.example.com</h1>
      <iframe src="http://b.example.com/iframe.html" id="myIframe"></iframe>
      <script>
      const iframe = document.getElementById('myIframe');
      iframe.onload = function() {
      // 发送消息给 iframe
      iframe.contentWindow.postMessage('Hello from a.example.com', 'http://b.example.com');
      };
      
      window.addEventListener('message', function(event) {
      // 检查消息来源
      if (event.origin !== 'http://b.example.com') {
      return;
      }
      
      console.log('Received message from b.example.com:', event.data);
      alert('Message received: ' + event.data);
      });
      </script>
      </body>
      </html>
    • b.example.com/iframe.html:

      <!DOCTYPE html>
      <html>
      <head>
      <title>b.example.com</title>
      </head>
      <body>
      <h1>b.example.com</h1>
      <script>
      window.addEventListener('message', function(event) {
      // 检查消息来源
      if (event.origin !== 'http://a.example.com') {
      return;
      }
      
      console.log('Received message from a.example.com:', event.data);
      
      // 发送消息给父窗口
      window.parent.postMessage('Hello from b.example.com', 'http://a.example.com');
      });
      </script>
      </body>
      </html>

    优点:

    • 安全可靠。
    • 支持双向通信。

    缺点:

    • 需要手动处理消息的发送和接收。

四、总结与建议

同源策略是前端安全的重要基石,但也是开发过程中常见的阻碍。理解同源策略的原理和边界,掌握常用的跨域解决方案,是每个前端工程师必备的技能。

建议:

  • 在开发中,优先考虑使用 CORS 解决跨域问题。
  • 如果需要兼容老版本的浏览器,可以考虑使用 JSONP。
  • 对于复杂的跨域场景,可以考虑使用代理服务器。
  • 在进行跨域通信时,一定要注意安全问题,防止 XSS 和 CSRF 攻击。
  • window.postMessage 是一种安全可靠的跨域通信机制,可以用于实现复杂的跨域交互。

希望今天的讲座能帮助大家更好地理解同源策略,并在开发中灵活运用各种跨域解决方案。以后遇到跨域问题,别再慌了,想想咱们今天讲的这些“十八般武艺”,总有一招能帮你搞定! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注