面试必备知识点:同源策略与跨域解决方案

232 阅读2分钟

在前端开发过程中,同源策略(Same-origin policy)是一个非常重要且基础的安全机制。它对于保护用户隐私以及保证Web应用的安全性有着至关重要的作用。然而,在实际项目开发中,我们经常会遇到需要跨域访问的情况。本文将详细介绍同源策略的概念以及常见的几种跨域解决方案,并且通过代码示例进行说明,帮助大家更好地理解和应对面试中的相关问题。

什么是同源策略?

同源策略是一种约定俗成的安全机制,它要求Web应用只能读写来自于同一来源的数据。这里的“来源”包括三个要素:协议(Protocol)、域名(Domain Name)和端口(Port)。只有这三个要素完全相同的情况下,两个Web应用才能被认为是“同源”的。

例如,一个页面http://192.168.3.1:3000/home和另一个页面http://192.168.3.1:8080/home,尽管IP地址相同,但由于端口不同,它们也被视为不同的源。

同源策略的存在主要是为了防止恶意脚本从一个站点读取另一个站点的敏感信息,如用户的个人信息或Cookie等。

跨域解决方案

1. JSONP(JSON with Padding)

JSONP是一种较早的跨域解决方案,它利用了<script>标签没有跨域限制的特点。前端通过创建一个<script>标签,将其src属性设置为目标URL,并在URL中附带一个回调函数名(通常称为callback),后端则会将数据包装在这个函数名内返回。
优点

  • 实现简单,兼容性好。

缺点

  • 只能实现GET请求。
  • 安全性较差,容易受到XSS攻击。

示例代码

(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>JSONP Example</title>
</head>
<body>
  <script>
    function jsonp(url, cb) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        window[cb] = function(data) {
          resolve(data);
        };
        script.src = `${url}?cb=${cb}`;
        document.body.appendChild(script);
      });
    }

    jsonp('http://localhost:3000', 'callback').then(res => {
      console.log(res);
    });
  </script>
</body>
</html>

(server.js)

const http = require('http');

http.createServer(function(req, res) {
  const query = new URL(req.url, `http://${req.headers.host}`).searchParams;
  if (query.get('cb')) {
    const cb = query.get('cb');
    const data = 'hello world';
    const result = `${cb}("${data}")`;
    res.end(result);
  }
}).listen(3000);

2. CORS(Cross-Origin Resource Sharing)

CORS是现代Web开发中最常用的一种跨域解决方案。它允许服务器通过设置特定的HTTP响应头(如Access-Control-Allow-Origin)来指定哪些源可以访问其资源。CORS提供了更细粒度的访问控制,并且支持各种类型的HTTP请求。
优点

  • 支持所有类型的HTTP请求。
  • 更好的安全性。

缺点

  • 需要后端的支持。

示例代码

(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CORS Example</title>
</head>
<body>
  <script>
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3001');
    xhr.send();
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
      }
    };
  </script>
</body>
</html>

(server.js)

const http = require('http');

http.createServer(function(req, res) {
  res.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET'
  });
  res.end('hello world');
}).listen(3001);

3. Nginx 作为代理

Nginx可以配置为一个反向代理,将前端的跨域请求先代理到Nginx服务器,再由Nginx服务器转发到目标服务器,这样可以绕过浏览器的同源策略限制。
优点

  • 配置灵活。

缺点

  • 需要额外的服务器资源。

示例代码

(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Nginx Proxy Example</title>
</head>
<body>
  <script>
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3001');
    xhr.send();
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
      }
    };
  </script>
</body>
</html>

(proxy.js)

const http = require('http');

http.createServer(function(req, res) {
  const options = {
    host: '192.168.1.63',
    port: '3000',
    path: '/',
    method: 'GET',
    headers: {}
  };

  http.request(options, proxyRes => {
    proxyRes.on('data', function(data) {
      res.end(data.toString());
    });
  }).end();
}).listen(3001);

4. WebSocket

WebSocket是一种双向通信协议,它允许客户端与服务器之间建立持久的连接。WebSocket协议本身不受同源策略的限制,因此可以用来实现跨域的数据传输。
优点

  • 实时性高,适合构建实时应用。

缺点

  • 需要服务器支持WebSocket协议。

示例代码

(index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocket Example</title>
</head>
<body>
  <script>
    function myWebSocket(url, params = {}) {
      return new Promise(function(resolve, reject) {
        const socket = new WebSocket(url);
        socket.onopen = () => {
          socket.send(JSON.stringify(params));
        };
        socket.onmessage = function(e) {
          resolve(e.data);
        };
      });
    }

    myWebSocket('ws://localhost:3000', {age: 18}).then(res => {
      console.log(res);
    });
  </script>
</body>
</html>

(ws-server.js)

const WebSocket = require('ws');

const ws = new WebSocket.Server({ port: 3000 });

ws.on('connection', function(obj) {
  obj.on('message', function(data) {
    obj.send('欢迎访问');
    setInterval(() => {
      obj.send(Date.now());
    }, 2000);
  });
});

5. postMessage

postMessage API是HTML5引入的一种跨域通信方式,它允许两个不同的窗口或iframe之间进行安全的消息传递。
优点

  • 支持多种消息格式。
  • 提供了更好的安全性。

缺点

  • 实现起来相对复杂

示例代码

父页面 (parent.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PostMessage Example</title>
</head>
<body>
  <h2>首页</h2>
  <iframe id="frame" src="http://127.0.0.1:5500/postMessage/detail.html" width="800" height="500" frameborder="0"></iframe>
  <script>
    document.domain = '127.0.0.1';
    document.getElementById('frame').onload = function() {
      this.contentWindow.postMessage({name: '小明', age: 18}, 'http://127.0.0.1:5500');
    };
  </script>
</body>
</html>

子页面 (detail.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Detail Page</title>
</head>
<body>
  <h3>详情页 -- <span id="title"></span> </h3>
  <script>
    document.domain = '127.0.0.1';
    let title = document.getElementById('title');
    window.onmessage = function(e) {
      title.innerText = `${e.data.name} ${e.data.age}岁`;
      e.source.postMessage(`小明现在${e.data.age + 1}岁`, e.origin);
    };
  </script>
</body>
</html>

6. document.domain

对于在同一个顶级域名下的多个子域来说,可以通过设置document.domain为相同的值来实现跨域访问。
优点

  • 在特定场景下非常实用

缺点

  • 应用场景有限

示例代码

父页面 (parent.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document Domain Example</title>
</head>
<body>
  <iframe id="frame" src="http://www.b.test.com/home" width="800" height="500" frameborder="0"></iframe>
  <script>
    document.domain = 'test.com';
    document.getElementById('frame').onload = function() {
      this.contentWindow.postMessage('Hello from parent domain!', 'http://www.b.test.com');
    };
  </script>
</body>
</html>

子页面 (home.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Home Page</title>
</head>
<body>
  <script>
    document.domain = 'test.com';
    window.onload = function() {
      window.addEventListener('message', function(e) {
        console.log('Received message:', e.data);
      });
    };
  </script>
</body>
</html>

总结

同源策略是Web安全的重要组成部分,了解并掌握其背后的原理对于每一个前端开发者都是必要的。在实际项目中,我们需要根据具体的业务需求选择合适的跨域解决方案。希望本文能帮助你在面试时更加自信地回答关于同源策略及跨域的相关问题。
如果觉得文章对你有帮助的话,请点赞收藏,感谢!