解释浏览器同源策略 (Same-Origin Policy) 及其安全意义,以及常见的跨域解决方案 (CORS, JSONP, Proxy)。

各位观众老爷,大家好!我是你们的浏览器同源策略老司机,今天咱们就来聊聊这浏览器安全界的“门神”——同源策略 (Same-Origin Policy)。这玩意儿听起来高大上,但其实就是浏览器为了保护咱们的隐私,防止恶意网站偷窥咱们的个人信息搞出来的规矩。

一、啥是同源?同源策略又是啥?

想象一下,你家小区门口有个保安大爷,他要核实来访者是不是你家亲戚朋友,才能放他们进来。同源策略就扮演着类似保安大爷的角色。

那啥是“同源”呢? 简单来说,两个网页的协议 (protocol)、域名 (domain) 和端口 (port) 都相同,就可以认为是同源的。 缺一不可!

元素 举例
协议 (Protocol) http, https
域名 (Domain) example.com, sub.example.com
端口 (Port) 80, 443, 8080

比如:

  • http://www.example.com/index.htmlhttp://www.example.com/script.js -> 同源
  • http://www.example.com/index.htmlhttps://www.example.com/index.html -> 不同源 (协议不同)
  • http://www.example.com/index.htmlhttp://sub.example.com/index.html -> 不同源 (域名不同)
  • http://www.example.com:80/index.htmlhttp://www.example.com:8080/index.html -> 不同源 (端口不同)

而同源策略就是浏览器的一项安全措施,它限制了一个源的文档或脚本如何才能去访问另一个源的资源。 也就是说,浏览器会阻止跨源的读写操作。 换句话说,如果两个页面不同源,那么A页面的Javascript就不能随意读取B页面里的数据,也不能随意操控B页面的DOM。

二、同源策略的具体限制

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

  1. Cookie、LocalStorage 和 IndexDB 无法跨域读取。 除非你设置了 Cookie 的 domain 属性允许跨子域访问。
  2. DOM 无法跨域操作。 A 页面里的 JS 没法直接修改 B 页面的 DOM 结构。
  3. AJAX 请求受到限制。 这就是我们最常遇到的跨域问题。 A 页面里的 JS 没法直接用 AJAX 向 B 页面发起请求。

三、同源策略的安全意义

为啥要有同源策略? 没了它会咋样?

想象一下,你同时登录了你的银行网站 bank.com 和一个看起来很不正经的网站 evil.com。 如果没有同源策略,evil.com 上的恶意脚本就可以通过 AJAX 访问 bank.com 的 API,窃取你的银行账户信息,甚至直接帮你转账!

同源策略就像一道防火墙,阻止了这种恶意行为的发生,保护了我们的隐私和安全。

四、突破同源策略的常见方法

虽然同源策略很重要,但有时候我们又不得不进行跨域操作。 比如,你的网站需要从另一个域名下的 API 获取数据。 这时候,我们就需要一些“曲线救国”的方法来突破同源策略的限制。

下面介绍几种常见的跨域解决方案:

  1. CORS (Cross-Origin Resource Sharing)

    CORS 是目前最主流、最推荐的跨域解决方案。 它的核心思想是在服务器端设置 HTTP 响应头,告诉浏览器允许哪些源的请求访问该资源。

    • 工作原理:

      浏览器在发起跨域请求时,会在请求头中添加一个 Origin 字段,表明请求来自哪个源。 服务器收到请求后,会根据 Origin 字段判断是否允许该请求。 如果允许,服务器会在响应头中添加 Access-Control-Allow-Origin 字段,指定允许的源。 浏览器收到响应后,会检查 Access-Control-Allow-Origin 字段,如果该字段包含当前源,或者设置为 * (表示允许所有源),那么浏览器就允许该请求。 否则,浏览器会阻止该请求。

    • 简单请求 vs 预检请求

      CORS 分为简单请求和预检请求。

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

        • 请求方法是 GETHEADPOST
        • HTTP 头信息不超过以下字段:AcceptAccept-LanguageContent-LanguageContent-Type (只限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。

        对于简单请求,浏览器会直接发起跨域请求,并在请求头中添加 Origin 字段。

      • 预检请求: 不满足简单请求条件的请求被称为预检请求。 比如,使用了 PUTDELETE 方法,或者 Content-Type 设置为 application/json

        对于预检请求,浏览器会先发起一个 OPTIONS 请求,询问服务器是否允许该跨域请求。 OPTIONS 请求的请求头中包含 Origin 字段,以及 Access-Control-Request-Method (表示实际请求的方法) 和 Access-Control-Request-Headers (表示实际请求的头信息) 字段。

        服务器收到 OPTIONS 请求后,会根据这些字段判断是否允许该请求。 如果允许,服务器会在响应头中添加以下字段:

        • Access-Control-Allow-Origin: 指定允许的源。
        • Access-Control-Allow-Methods: 指定允许的 HTTP 方法。
        • Access-Control-Allow-Headers: 指定允许的 HTTP 头信息。
        • Access-Control-Max-Age: 指定预检请求的有效期 (单位为秒)。

        浏览器收到 OPTIONS 响应后,会检查这些字段,如果允许该跨域请求,才会发起实际的请求。

    • 服务器端配置示例 (Node.js + Express):

      const express = require('express');
      const cors = require('cors');  // 引入 cors 中间件
      const app = express();
      
      // 允许所有来源的跨域请求
      app.use(cors());
      
      // 或者,更精确地控制允许的来源
      // const corsOptions = {
      //   origin: 'http://www.example.com'  // 允许 example.com 的请求
      // };
      // app.use(cors(corsOptions));
      
      app.get('/data', (req, res) => {
        res.json({ message: 'Hello from the server!' });
      });
      
      app.listen(3000, () => {
        console.log('Server listening on port 3000');
      });
    • 服务器端配置示例 (Java + Spring Boot):

      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.CorsRegistry;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
      @Configuration
      public class CorsConfig implements WebMvcConfigurer {
      
          @Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/**")  // 允许所有路径
                      .allowedOrigins("http://www.example.com") // 允许 example.com
                      .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的方法
                      .allowedHeaders("*")  // 允许的头
                      .allowCredentials(true) // 是否允许发送 Cookie
                      .maxAge(3600); // 预检请求的有效期
          }
      }
    • 优点:

      • 标准化的解决方案,被所有现代浏览器支持。
      • 灵活性高,可以精确控制允许的源、方法和头信息。
      • 安全性高,服务器端可以对请求进行验证。
    • 缺点:

      • 需要服务器端配合,修改响应头。
      • 对于不支持 CORS 的老旧浏览器,无法使用。
  2. JSONP (JSON with Padding)

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

    • 工作原理:

      客户端定义一个回调函数,然后通过 <script> 标签向服务器发起请求。 服务器端将数据包装在一个函数调用中,并将该函数调用返回给客户端。 客户端收到响应后,会执行该函数调用,从而获取到数据。

    • 客户端代码示例:

      <!DOCTYPE html>
      <html>
      <head>
      <title>JSONP Example</title>
      </head>
      <body>
      <script>
          function handleResponse(data) {
              console.log('Received data:', data);
              // 处理数据
          }
      
          function loadData() {
              var script = document.createElement('script');
              script.src = 'http://api.example.com/data?callback=handleResponse'; // 注意这里的 callback 参数
              document.body.appendChild(script);
          }
      
          loadData();
      </script>
      </body>
      </html>
    • 服务器端代码示例 (Node.js + Express):

      const express = require('express');
      const app = express();
      
      app.get('/data', (req, res) => {
          const callback = req.query.callback;
          const data = { message: 'Hello from the server!' };
          const jsonp = `${callback}(${JSON.stringify(data)})`;  // 构造 JSONP 响应
          res.type('application/javascript'); // 设置 Content-Type
          res.send(jsonp);
      });
      
      app.listen(3000, () => {
          console.log('Server listening on port 3000');
      });
    • 优点:

      • 兼容性好,支持老旧浏览器。
      • 简单易用,客户端和服务端代码都比较简单。
    • 缺点:

      • 只支持 GET 请求。
      • 安全性较差,容易受到 XSS 攻击 (如果服务器端没有对数据进行严格的过滤)。
      • 需要服务器端配合,修改响应格式。
  3. Proxy (代理)

    Proxy 是一种比较通用的跨域解决方案。 它的核心思想是在服务器端设置一个代理服务器,客户端先向代理服务器发起请求,然后代理服务器再向目标服务器发起请求,并将结果返回给客户端。

    • 工作原理:

      由于代理服务器和客户端是同源的,所以客户端可以毫无阻碍地向代理服务器发起请求。 而代理服务器和目标服务器之间的通信,不受同源策略的限制,因为这是服务器之间的通信,不是浏览器行为。

    • 实现方式:

      • 反向代理: 在服务器端使用 Nginx 或 Apache 等 Web 服务器配置反向代理。
      • 中间层代理: 使用 Node.js 等后端技术搭建一个中间层服务器,作为代理。
    • 反向代理配置示例 (Nginx):

      server {
          listen 80;
          server_name www.example.com;  // 你的域名
      
          location /api/ {  //  /api/ 开头的请求会被代理
              proxy_pass http://api.another-domain.com/;  // 目标服务器地址
              proxy_set_header Host $host;  // 传递 Host 头
              proxy_set_header X-Real-IP $remote_addr;  // 传递客户端 IP
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  // 传递 X-Forwarded-For 头
          }
      }

      客户端代码只需要访问 http://www.example.com/api/xxx 即可,Nginx 会将请求转发到 http://api.another-domain.com/xxx

    • 中间层代理代码示例 (Node.js + Express + http-proxy-middleware):

      const express = require('express');
      const { createProxyMiddleware } = require('http-proxy-middleware'); // 引入中间件
      const app = express();
      
      app.use('/api', createProxyMiddleware({
          target: 'http://api.another-domain.com',  // 目标服务器地址
          changeOrigin: true,  //  必须设置,否则会报错
          pathRewrite: {  // 可选:重写路径
              '^/api': ''  //  将 /api 替换为空字符串
          },
      }));
      
      app.listen(3000, () => {
          console.log('Proxy server listening on port 3000');
      });

      客户端代码只需要访问 http://localhost:3000/api/xxx 即可,该代理服务器会将请求转发到 http://api.another-domain.com/xxx

    • 优点:

      • 客户端代码无需修改。
      • 可以解决各种跨域问题,包括 Cookie、LocalStorage 等。
      • 安全性较高,可以在代理服务器端对请求进行验证和过滤。
    • 缺点:

      • 需要搭建代理服务器,增加服务器端的负担。
      • 可能会增加网络延迟。

五、一些补充说明

  • document.domain: 只适用于二级域名相同的情况。 比如,a.example.comb.example.com 可以通过设置 document.domain = 'example.com' 来实现跨域。 但这种方法已经逐渐被淘汰,不推荐使用。

  • window.postMessage: 允许不同源的页面之间进行通信。 但需要双方都监听 message 事件,并对消息进行验证,以防止恶意代码利用。 安全性较高,但实现起来比较繁琐。

  • iframe 跨域: 可以使用 postMessage 或者 URL 的 hash 值进行通信。

六、总结

同源策略是浏览器安全的重要基石,它有效地保护了我们的隐私和安全。 但在实际开发中,我们常常需要进行跨域操作。 CORS 是目前最主流、最推荐的跨域解决方案。 JSONP 适用于老旧浏览器,但安全性较差。 Proxy 是一种通用的解决方案,但需要搭建代理服务器。

选择哪种跨域解决方案,取决于具体的场景和需求。 在选择方案时,需要权衡安全性、兼容性和易用性。

好了,今天的讲座就到这里。 希望大家对浏览器同源策略有了更深入的了解。 如果还有什么疑问,欢迎提问! 咱们下期再见!

发表回复

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