什么是跨域
相信很多做前端开发的同学都在浏览器控制台遇到过如下错误。
1 | Access to XMLHttpRequest at 'http://localhost:3000/api/xxx' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. |
这个错误是由于浏览器的同源策略导致的,同源策略是浏览器的一种安全策略,它要求浏览器只能向同源网址发送请求,同源网址指的是协议、域名、端口号都相同的网址。
以下几种情况都不同源,都会导致跨域。
- 域名不同
remotehost
vslocalhost
1
http://localhost:4200 和 http://remotehost:3000
- 协议不同
http
vshttps
1
http://localhost:3000 和 https://localhost:3000
- 端口不同
3000
vs4200
1
http://localhost:3000 和 http://localhost:4200
文章开头的错误消息中,http://localhost:4200
(4200是Angular项目常用端口)和http://localhost:3000
(3000是Node.js项目常用端口)就不是同源网址,因为它们的端口号不同。
同源策略的目的是为了防止恶意的网站窃取数据,但是对于前端开发来说,这个策略有时候就显得有点过于严格了,比如我们在开发时,前端项目和后端项目往往是分开的,前端项目一般运行在http://localhost:4200
,而后端项目一般运行在http://localhost:3000
,这样就导致了前端项目无法向后端项目发送请求,从而导致了上面的错误。那么如何解决这个问题呢?我们可以在后端项目中启用CORS,从而解决这个问题。下面我们就来看看如何在Node和Express中启用CORS。
什么是CORS
CORS是Cross-Origin Resource Sharing的缩写,中文名是跨域资源共享,它是一种机制,它使用额外的HTTP头来告诉浏览器,允许运行在一个源上的Web应用访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,会发起一个跨域HTTP请求。
哪些请求会使用CORS呢?
- Fetch与XMLHttpRequest
- Web Fonts,@font-face within CSS
- WebGL textures
简单请求与非简单请求
为什么要介绍简单请求和非简单请求呢?因为对于简单请求和非简单请求,浏览器的处理方式是不同的。
- 简单请求, 浏览器会自动处理跨域请求,不需要额外的处理。
- 非简单请求,浏览器会先发送一个OPTIONS请求,询问服务器是否允许跨域请求,如果服务器允许,浏览器才会发送真正的请求。
简单请求:
满足以下条件的请求是简单请求。
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值:
application/x-www-form-urlencoded
,multipart/form-data
,text/plain
非简单请求
除了简单请求,都是非简单请求。
Preflight请求
又称为预检请求,在非简单请求之前,浏览器会发送一个preflight请求,询问后端服务器是否支持跨域,preflight请求都是Option请求,下面是一个列子。
- General块中可以看到,预检请求用的是
OPTIONS
请求,而且返回值是200,说明请求成功了。
General
1 | Request URL: http://10.10.143.144:9898/bff/api/v1/application/sysVariable/list?_t=1641265683731 |
- Request Header区块中Access-Control-Request-Headers指定的是GET请求,说明接下来要进行的跨域请求是GET请求,而且有自定义请求头,放在Access-Control-Request-Headers字段中,请求的来源是Origin字段标明的,是
http://localhost:3001
,表示当前正在本机进行调试。
Request Header
1 | Accept: */* |
- Response Header区块中反应的是OPTIONS请求后的结果,图中四个标红加粗的字段表示服务器的跨域请求设置
- Access-Control-Allow-Headers表示允许的自定义请求头。
- Access-Control-Allow-Methods表示允许的请求方法
- Access-Control-Allow-Origin表示允许的跨域请求的来源
- Access-Control-Max-Age表示预检请求的缓存时间,在这个时间内,如果再发生跨域请求,则无需发送预检请求。
Response Header
1 | Access-Control-Allow-Headers: app, cache-control, dcid, nounce, timestamp, userid, uuid |
解决跨域
解决跨域的方法很多,常用的有以下几种:
后端开启CORS
根据同源策略,我们可以在后端设置Access-Control-Allow-Origin
这个响应头,来允许指定的域名访问该资源。下面我们来看看如何在Node和Express中启用CORS。
- 创建一个Express项目并安装cors
1
2
3npm init
npm install express --save
npm install cors --save - 在项目根目录下创建
server.js
文件,添加如下内容,这个服务器提供两个接口,一个是/
,一个是/users
,其中/users
接口返回一个用户列表。而且在server.js
中启用了CORS,允许http://localhost:4200
这个地址访问该服务提供的接口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;
// Enable CORS
let corsOptions = {
origin : ['http://localhost:4200'], // 前端项目地址
};
app.use(cors(corsOptions));
app.get('/', (req, res) => {
res.send('Hello World!')
});
app.get('/users', (req, res) => {
const users = [
{name: 'John', age: 30},
{name: 'Jane', age: 20},
];
res.json(users);
}); - 启动项目
1
node server.js
- 新建一个前端项目(Angular),启动项目后运行在localhost:4200,通过前端访问这个api时就不会有跨域问题了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {Component, OnInit} from '@angular/core';
import {UsersModel} from "./users.model";
import {HttpClient} from "@angular/common/http";
({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.less']
})
export class UsersComponent implements OnInit {
users: UsersModel[] = [];
constructor(private http: HttpClient) {
}
ngOnInit(): void {
this.http.get<UsersModel[]>('http://localhost:3000/users')
.subscribe((users: UsersModel[]) => {
this.users = users;
});
}
} - 记得在app.module.ts中启用HttpClientModule
1
2
3
4
5
6
7
8
9
10
11
12import {HttpClientModule} from '@angular/common/http';
({
declarations: [
AppComponent,
],
imports: [
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
使用JsonP
JSONP (JSON with Padding) 是一种跨域数据交互协议,它允许页面从不同的域名下获取数据。其实现跨域的原理主要基于浏览器对<script>
标签的宽松政策,即浏览器允许页面通过<script>
标签加载并执行来自任何来源(即任何域名)的JavaScript代码。
浏览器对一些html标签允许跨域访问,比如<img>
、<link>
、<script>
等,详情参考这里
以下是JSONP实现跨域的基本步骤:
创建
<script>
标签:在需要请求数据的网页中动态创建一个<script>
标签,并设置其src
属性为要请求的数据接口地址。这个地址通常会包含一个回调函数名作为参数。定义回调函数:在网页中定义一个JavaScript函数,该函数的名字就是之前在
src
属性中指定的回调函数名。当服务器响应返回时,这个函数会被调用,且响应的数据会作为参数传递给这个函数。服务器端响应:服务器接收到请求后,会将数据包装在一个函数调用中返回。这个函数名就是客户端请求中指定的那个回调函数名。例如,如果回调函数名为
handleResponse
,而返回的数据是{"name": "John"}
,那么服务器可能会返回如下内容:1
handleResponse({"name": "John"});
执行回调函数:由于
<script>
标签加载的是一个有效的JavaScript脚本,所以浏览器会执行这个脚本,即执行handleResponse
函数,并将数据作为参数传入。这样,客户端就可以处理从服务器接收到的数据了。
JsonP实现示意图。
JSONP的主要优点是简单易用,不需要特殊的服务器配置,且几乎所有的浏览器都支持。然而,它也存在一些限制和安全风险:
- 仅支持GET请求:JSONP只能发起GET请求,无法使用POST等其他HTTP方法。
- 安全性问题:因为JSONP本质上是在执行来自外部源的任意JavaScript代码,所以存在注入攻击的风险。必须确保数据来源可靠。
- 错误处理困难:JSONP没有标准的错误处理机制,一旦请求失败,很难确定失败的原因。
因此,在选择是否使用JSONP时,开发者需要权衡其带来的便利性和潜在的安全风险。随着CORS(跨源资源共享)等更现代的解决方案的出现,JSONP的使用正在逐渐减少。
Angular中HttpClient模块也提供了JSONP的支持,使用方法如下:
1 | import { Component, OnInit } from '@angular/core'; |
前端使用反向代理
这种方法一般是开发阶段使用的,因为本质上,前端是无法解决跨域问题的,只能通过后端来解决。
Angular项目
Angular项目中可以使用proxy.conf.json
文件配置反向代理,然后在angular.json
或者project.json
(基于Nx的Mono repo)中配置proxyConfig
指向该文件即可。
proxy.conf.json
文件内容如下:
1
2
3
4
5
6{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}
angular.json
或project.json
文件内容如下:
1
2
3
4
5
6"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
}
}
使用浏览器插件
这个不做过多介绍,大家自行探索
使用非安全模式启动浏览器
在Windows系统上,可以通过以下命令启动Chrome浏览器,这样就可以绕过浏览器的同源策略。这种方式也不推荐。
1 | chrome.exe --user-data-dir="C://Chrome dev session" --disable-web-security |