同源策略

同源策略是来自浏览器的,为保障浏览器执行javascript代码中访问后端API请求的 协议 域名 端口 与前端服务器相同而设置的策略,此策略是用来确保安全的。

比较项 描述 示例 是否同源
协议 比较两个URL的协议是否相同 http://example.com vs. https://example.com 否(协议不同)
域名 比较两个URL的域名是否相同 http://example.com vs. http://sub.example.com 否(域名不同)
端口 比较两个URL的端口是否相同 http://example.com:80 vs. http://example.com:8080 否(端口不同)
同源示例 完全相同的协议、域名和端口 http://example.com:80 vs. http://example.com:80 是(完全相同)
协议不同 只有协议不同,但域名和端口相同 http://example.com:80 vs. https://example.com:80 否(协议不同)
子域名不同 主域名相同,但子域名不同 http://example.com vs. http://sub.example.com 否(子域名不同)
端口不同 协议和域名相同,但端口不同 http://example.com:80 vs. http://example.com:8080 否(端口不同)
跨域访问 不同源之间的访问 http://example.com vs. http://anotherdomain.com 否(域名不同)
文件访问 同一域名但不同文件之间的访问 http://example.com/dir/page.html vs. http://example.com/dir2/otherpage.html 是(同一域名)
协议、域名、端口相同 完全相同的协议、域名和端口 https://example.com vs. https://example.com 是(完全相同)

为什么要跨域?

你的应用开发选择了前后端分离的方案,导致前端和后端不是同源的情况下,此时需要前端跨域访问后端。

解决方案

代理

Nginx(静态服务器代理)

使用Nginx服务器代理访问后端服务器,一般前端的静态文件放置于Niginx服务器目录下,此时的后端服务器可能与前端在同一服务器,也可以是不同的服务器,但经过Niginx会将前端请求带有类似/api的请求发送到后端服务器,将避免浏览器同源策略。

后端服务器代理

使用 Node.js 和 Express

后端服务器将检测拦截浏览器带有类似/api的请求,将其代理至指定后端服务器https://target-server.com

假设我们使用 Node.js 和 Express 来设置一个简单的代理服务器。

  1. 安装所需的 npm 包

首先,需要安装 expresshttp-proxy-middleware

1
npm install express http-proxy-middleware
  1. 创建代理服务器

创建一个 server.js 文件并添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 目标服务器的 URL
const targetUrl = 'https://target-server.com';

// 创建代理中间件
const proxy = createProxyMiddleware({
target: targetUrl,
changeOrigin: true, // 更改请求中的源头
pathRewrite: { '^/api': '' }, // 将 '/api' 路径重写为空,这样 '/api/endpoint' 将被转发到 'https://target-server.com/endpoint'
onProxyReq: (proxyReq, req, res) => {
// 可以在这里修改请求,比如添加头信息
proxyReq.setHeader('X-Added', 'foobar');
}
});

// 使用代理中间件
app.use('/api', proxy);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Proxy server is running on port ${PORT}`);
});
  1. 运行代理服务器

在终端中运行代理服务器:

1
node server.js
  1. 前端请求

在前端代码中,将请求的 URL 更改为代理服务器的地址。例如,如果代理服务器运行在 http://localhost:3000,则前端代码可以这样写:

1
2
3
4
5
6
7
fetch('http://localhost:3000/api/endpoint', {
method: 'GET',
credentials: 'include' // 如果需要携带凭证
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

前端服务器代理

此时的前端服务器是可以处理逻辑的,而不是静态服务器那样只负责响应静态件,这样的环境其实就是前端开发环境下的web服务器,当dev环境下启动前端web服务器就可以支持代理服务器的逻辑(使用 webpack-dev-server 配置代理),此时无论是请求前端服务器/后端API都是同源。

webpack-dev-server 作为一个 Node.js 服务器运行在本地,它使用 webpack 来打包和提供静态资源,并提供了一些额外的开发工具和功能。以下是它的基本工作流程:

  1. 启动服务器webpack-dev-server 启动一个本地服务器,通常在 localhost 和指定的端口(如 http://localhost:9000)上运行。
  2. 打包资源:使用 webpack 将前端代码打包成可供浏览器使用的静态文件(如 HTML、CSS、JavaScript)。
  3. 提供静态资源:将打包后的静态文件提供给浏览器访问。
  4. 代理请求:通过代理功能,将某些请求转发到其他服务器(例如后端 API 服务器),以解决跨域问题。
  5. 热模块替换(HMR):在代码变更时,自动重新加载或热更新页面,而不需要手动刷新浏览器。
配置和使用

以下是一个典型的 webpack.config.js 文件,配置了 webpack-dev-server 和代理功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
historyApiFallback: true, // 适用于单页面应用,所有 404 响应都被替代为 index.html
proxy: {
'/api': {
target: 'https://target-server.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
secure: false // 如果目标服务器使用自签名证书
}
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
};
启动开发服务器

在项目根目录运行以下命令启动 webpack-dev-server

1
2

npx webpack serve
前端请求示例

在前端代码中,可以直接向 /api 发送请求,不需要考虑跨域问题:

1
2
3
4
5
6
7
fetch('/api/endpoint', {
method: 'GET',
credentials: 'include' // 如果需要发送凭证(如 Cookies)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

总结

  • 运行环境webpack-dev-server 运行在本地 Node.js 环境中,不是在浏览器端运行。
  • 功能:它提供了静态资源服务器、代理功能、热模块替换等,帮助前端开发者加速开发流程。
  • 跨域解决:通过代理功能,webpack-dev-server 可以将前端的请求转发到目标服务器,从而解决跨域问题。

CORS(跨域资源共享)

跨域资源共享,从字面意思理解是不同前端共享后端服务资源,但需要进行跨域访问才可以实现其特性。
后端添加服务器响应头,类似Access-Control-Allow-开头的响应头来允许哪些前端可以进行访问此后端,实现此后端资源允许共享给某些前端服务器的IP/域名,换句话说,此响应头的作用是为了防止其他不明来历的前端IP/域名滥用我们的后端资源。

CORS 策略是浏览器实现的,后端只需要允许使用即可。

  1. 允许的域名

    • 可以指定一个特定的域名,也可以使用 * 来允许所有域名,会造成接口的滥用等安全风险。
    1
    2
    <!-- 只允许来自https://example.com前端的脚本访问本后端资源 -->
    Access-Control-Allow-Origin: https://example.com
  2. 允许的 HTTP 方法

    • 使用 Access-Control-Allow-Methods 头字段来指定允许的 HTTP 方法,如 GET、POST、PUT、DELETE 等。
    1
    2

    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  3. 允许的请求头

    • 使用 Access-Control-Allow-Headers 头字段来指定允许的自定义请求头。
    1
    2

    Access-Control-Allow-Headers: Content-Type, Authorization
  4. 允许携带凭证(如 Cookie)

    • 使用 Access-Control-Allow-Credentials 头字段来指示是否允许携带凭证。
    1
    2

    Access-Control-Allow-Credentials: true
  5. 预检请求缓存时间

    • 使用 Access-Control-Max-Age 头字段来指定预检请求(OPTIONS 请求)的结果可以缓存多长时间(以秒为单位)。
    1
    2

    Access-Control-Max-Age: 3600

示例

假设后端使用 Express.js 来设置 CORS 头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require('express');
const app = express();

// 设置CORS头部
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
next();
});

// 示例路由
app.get('/api/data', (req, res) => {
res.json({ message: 'This is a CORS-enabled response' });
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});
处理预检请求

当浏览器发送跨域请求时,特别是对于非简单请求(如使用自定义头部、PUT 或 DELETE 方法等),会首先浏览器自动发送一个预检请求(OPTIONS 请求)来检查服务器是否允许该跨域请求。

1
2
3
4
5
6
7
8
// 处理预检请求
app.options('/api/data', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.sendStatus(204);
});
前端请求配置

在前端发送跨域请求时,确保将 credentials 设置为 include 以发送 Cookie 或其他凭证。

1
2
3
4
5
6
7
fetch('https://your-backend-domain.com/api/data', {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

通过以上配置,可以实现跨域请求,并且确保在响应头中正确设置了 CORS 头部,从而允许特定域名的跨域访问。