《基于 Koa 的登录页面实战:跨域》

249 阅读5分钟

《基于 Koa 的登录页面实战:跨域》

同源策略(Same-Origin Policy)

浏览器的一项重要安全策略。

其主要目的是防止不同源的文档或脚本之间的交互,以保护用户的隐私和安全。

同源的定义包括协议、域名和端口都相同。例如,http://example.com:80 和 https://example.com:443 不是同源,http://example.com 和 http://sub.example.com 也不是同源。

同源策略的限制主要体现在以下几个方面:

  1. Cookie、LocalStorage 和 IndexedDB 等存储数据无法在不同源之间共享。
  2. DOM 无法被不同源的脚本访问和操作。
  3. Ajax 请求受到限制,无法向不同源的服务器发送请求获取数据。

同源策略虽然保障了安全,但也给一些合理的跨域需求带来了挑战,因此出现了如 CORS 等解决跨域问题的技术和方法。

例如,如果一个网页来自 http://example1.com ,它里面的脚本就不能读取或修改来自 http://example2.com 的页面内容。

为什么会出现跨域?

跨域问题出现的主要原因是浏览器的同源策略(Same-Origin Policy)。

同源策略是一种安全机制,它要求浏览器中脚本的访问请求必须满足以下三个条件相同,才能被认为是同源的:

  1. 协议(Protocol):例如 http 或 https 。

  2. 域名(Domain):包括主域名和子域名。

  3. 端口(Port):默认端口(如 http 的 80 端口,https 的 443 端口)。

如果请求不满足同源条件,就会被认为是跨域请求。

出现这种限制主要是出于安全考虑,防止以下潜在的风险:

  1. 防止恶意网站窃取用户在其他网站上的敏感信息,如登录凭证、个人数据等。

  2. 避免恶意网站修改其他网站的页面内容或行为。

例如,如果没有同源策略,一个恶意网站可以通过 JavaScript 向用户经常访问的银行网站发送请求,获取用户的账户信息,或者在用户不知情的情况下向其他网站提交有害的数据。

如何解决跨域?

常见的解决跨域的方法

  1. CORS(跨源资源共享) :这是一种在服务器端设置响应头,告知浏览器允许哪些源可以访问资源的方法。服务器通过设置 Access-Control-Allow-Origin 等响应头来控制跨域请求的权限。

    • 原理:浏览器在发送跨域请求时会先发送一个“预检”请求(OPTIONS 方法),服务器返回允许的请求方法、头信息等,浏览器根据这些信息决定是否发送实际请求。
  2. JSONP(JSON with Padding) :利用 <script> 标签的跨域能力,服务器返回一个包含回调函数调用的 JavaScript 代码片段。

    • 原理:<script> 标签的 src 属性不受同源策略限制,通过动态创建 <script> 标签并指定跨域的 URL,服务端返回数据时包裹在指定的回调函数中,从而在前端获取到数据。
  3. 代理服务器:在前端和后端之间设置一个代理服务器,让前端向代理服务器发送请求,由代理服务器向目标服务器请求数据并返回给前端。

    • 原理:对于浏览器来说,它与代理服务器之间的通信不存在跨域问题,而代理服务器与目标服务器之间的通信在同一域内。

CORS 解决跨域的具体示例:

const Koa = require('koa');
const cors = require('@koa/cors');

const app = new Koa();

app.use(cors({
  origin: 'http://your-allowed-origin.com',  // 允许的源
  methods: ['GET', 'POST', 'PUT', 'DELETE'],  // 允许的请求方法
  allowedHeaders: ['Content-Type', 'Authorization'],  // 允许的请求头
}));

app.listen(3000);

JSONP 示例

前端部分:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    // 定义一个名为 handleData 的函数,用于接收和处理从服务器返回的数据
    function handleData(data) { 
      console.log(data); 
    }
  </script>
  <!-- 
    动态创建一个 <script> 标签,其 src 属性指向服务器的 URL 
    并携带了回调函数名 callback=handleData
    浏览器会向该 URL 发送请求获取数据
  -->
  <script src="http://example.com/data?callback=handleData"></script>
</body>

</html>

后端部分(Node.js 和 Express 框架):

const express = require('express'); 
// 导入 Express 框架,用于创建服务器和处理路由

const app = express(); 
// 创建一个 Express 应用实例

app.get('/data', (req, res) => { 
  // 定义一个处理 '/data' 路径的 GET 请求的路由处理函数
  const callbackName = req.query.callback; 
  // 从请求的查询参数中获取名为 'callback' 的值,并将其赋值给 callbackName

  const data = { message: 'Hello from the server!' }; 
  // 定义要返回给前端的数据对象

  const script = `${callbackName}(${JSON.stringify(data)})`; 
  // 构建包含回调函数调用和数据的 JavaScript 脚本字符串

  res.send(script); 
  // 将构建好的脚本字符串发送给前端
});

app.listen(3000, () => { 
  // 启动服务器,监听 3000 端口
  console.log('Server is running on port 3000');
});

代理服务器示例(使用 Node.js 的 http-proxy-middleware 库)

const express = require('express'); 
// 导入 Express 框架,用于创建服务器和处理路由等功能

const { createProxyMiddleware } = require('http-proxy-middleware'); 
// 从 'http-proxy-middleware' 库中导入创建代理中间件的相关功能

const app = express(); 
// 创建一个 Express 应用实例

app.use('/api', createProxyMiddleware({ 
  // 为 '/api' 路径设置代理中间件
  target: 'http://target-server.com', 
  // 定义代理的目标服务器地址
  changeOrigin: true, 
  // 允许更改请求的源信息,以模拟同源请求
  onProxyRes: (proxyRes, req, res) => { 
    // 定义当接收到目标服务器响应时的处理函数
    // 处理状态码
    if (proxyRes.statusCode === 500) { 
      // 如果目标服务器返回的状态码是 500
      proxyRes.statusCode = 502; 
      // 将状态码修改为 502
      res.end('服务暂时不可用,请稍后再试'); 
      // 结束响应,并向客户端发送自定义的错误消息
      return; 
      // 结束当前处理逻辑
    }

    // 处理头信息
    delete proxyRes.headers['Server']; 
    // 从响应头中删除 'Server' 字段
    proxyRes.headers['X-Proxy-Added'] = 'This header is added by the proxy'; 
    // 向响应头添加自定义字段

    // 处理主体内容(假设将文本转换为大写)
    let body = ''; 
    // 初始化一个空字符串用于存储响应主体数据
    proxyRes.on('data', (chunk) => { 
      // 当接收到响应的数据块时
      body += chunk; 
      // 将数据块添加到 body 字符串中
    });
    proxyRes.on('end', () => { 
      // 当响应数据接收完成时
      res.send(body.toUpperCase()); 
      // 将处理后的大写主体数据发送给客户端
    });
  }
}));

app.listen(8080); 
// 启动服务器,并监听 8080 端口