0%

JavaScript CORS

什么是跨域

相信很多做前端开发的同学都在浏览器控制台遇到过如下错误。

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.

这个错误是由于浏览器的同源策略导致的,同源策略是浏览器的一种安全策略,它要求浏览器只能向同源网址发送请求,同源网址指的是协议、域名、端口号都相同的网址。

以下几种情况都不同源,都会导致跨域。

  1. 域名不同
    remotehost vs localhost
    1
    http://localhost:4200 和 http://remotehost:3000
  2. 协议不同
    http vs https
    1
    http://localhost:3000 和 https://localhost:3000
  3. 端口不同
    3000 vs 4200
    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请求,询问服务器是否允许跨域请求,如果服务器允许,浏览器才会发送真正的请求。

简单请求:

满足以下条件的请求是简单请求。

  1. 请求方法是以下三种方法之一:
    • HEAD
    • GET
    • POST
  2. 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请求,下面是一个列子。

  1. General块中可以看到,预检请求用的是 OPTIONS请求,而且返回值是200,说明请求成功了。

General

1
2
3
4
5
Request URL: http://10.10.143.144:9898/bff/api/v1/application/sysVariable/list?_t=1641265683731
**Request Method**: OPTIONS
Status Code: 200 OK
Remote Address: 10.10.143.144:9898
Referrer Policy: strict-origin-when-cross-origin
  1. Request Header区块中Access-Control-Request-Headers指定的是GET请求,说明接下来要进行的跨域请求是GET请求,而且有自定义请求头,放在Access-Control-Request-Headers字段中,请求的来源是Origin字段标明的,是http://localhost:3001,表示当前正在本机进行调试。

Request Header

1
2
3
4
5
6
7
8
9
10
11
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7
**Access-Control-Request-Headers**: app,cache-control,dcid,nounce,timestamp,userid,uuid
**Access-Control-Request-Method**: GET
Connection: keep-alive
Host: 10.10.143.144:9898
**Origin**: http://localhost:3001
Referer: http://localhost:3001/
Sec-Fetch-Mode: cors
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
  1. Response Header区块中反应的是OPTIONS请求后的结果,图中四个标红加粗的字段表示服务器的跨域请求设置
    • Access-Control-Allow-Headers表示允许的自定义请求头。
    • Access-Control-Allow-Methods表示允许的请求方法
    • Access-Control-Allow-Origin表示允许的跨域请求的来源
    • Access-Control-Max-Age表示预检请求的缓存时间,在这个时间内,如果再发生跨域请求,则无需发送预检请求。

Response Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Access-Control-Allow-Headers: app, cache-control, dcid, nounce, timestamp, userid, uuid
Access-Control-Allow-Methods: PUT,DELETE,GET,POST,OPTIONS
Access-Control-Allow-Origin: http://localhost:3001
Access-Control-Expose-Headers: access-control-allow-headers, access-control-allow-methods, access-control-allow-origin, access-control-max-age, X-Frame-Options
Access-Control-Max-Age: 3600
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Cache-Control: no-store
Connection: keep-alive
Content-Length: 0
Date: Tue, 04 Jan 2022 03:08:03 GMT
Pragma: no-cache
Server: nginx/1.17.5
Strict-Transport-Security: max-age=8995000; includeSubdomains
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block

解决跨域

解决跨域的方法很多,常用的有以下几种:

后端开启CORS

根据同源策略,我们可以在后端设置Access-Control-Allow-Origin这个响应头,来允许指定的域名访问该资源。下面我们来看看如何在Node和Express中启用CORS。

  1. 创建一个Express项目并安装cors
    1
    2
    3
    npm init
    npm install express --save
    npm install cors --save
  2. 在项目根目录下创建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
    22
    const 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);
    });
  3. 启动项目
    1
    node server.js
  4. 新建一个前端项目(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
    25
    import { 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";

    @Component({
    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;
    });
    }
    }
  5. 记得在app.module.ts中启用HttpClientModule
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import {HttpClientModule} from '@angular/common/http';

    @NgModule({
    declarations: [
    AppComponent,
    ],
    imports: [
    HttpClientModule,
    ],
    providers: [],
    bootstrap: [AppComponent]
    })

使用JsonP

JSONP (JSON with Padding) 是一种跨域数据交互协议,它允许页面从不同的域名下获取数据。其实现跨域的原理主要基于浏览器对<script>标签的宽松政策,即浏览器允许页面通过<script>标签加载并执行来自任何来源(即任何域名)的JavaScript代码。

浏览器对一些html标签允许跨域访问,比如<img><link><script>等,详情参考这里

以下是JSONP实现跨域的基本步骤:

  1. 创建<script>标签:在需要请求数据的网页中动态创建一个<script>标签,并设置其src属性为要请求的数据接口地址。这个地址通常会包含一个回调函数名作为参数。

  2. 定义回调函数:在网页中定义一个JavaScript函数,该函数的名字就是之前在src属性中指定的回调函数名。当服务器响应返回时,这个函数会被调用,且响应的数据会作为参数传递给这个函数。

  3. 服务器端响应:服务器接收到请求后,会将数据包装在一个函数调用中返回。这个函数名就是客户端请求中指定的那个回调函数名。例如,如果回调函数名为handleResponse,而返回的数据是{"name": "John"},那么服务器可能会返回如下内容:

    1
    handleResponse({"name": "John"});
  4. 执行回调函数:由于<script>标签加载的是一个有效的JavaScript脚本,所以浏览器会执行这个脚本,即执行handleResponse函数,并将数据作为参数传入。这样,客户端就可以处理从服务器接收到的数据了。

JsonP实现示意图。
JSONP

JSONP的主要优点是简单易用,不需要特殊的服务器配置,且几乎所有的浏览器都支持。然而,它也存在一些限制和安全风险:

  • 仅支持GET请求:JSONP只能发起GET请求,无法使用POST等其他HTTP方法。
  • 安全性问题:因为JSONP本质上是在执行来自外部源的任意JavaScript代码,所以存在注入攻击的风险。必须确保数据来源可靠。
  • 错误处理困难:JSONP没有标准的错误处理机制,一旦请求失败,很难确定失败的原因。

因此,在选择是否使用JSONP时,开发者需要权衡其带来的便利性和潜在的安全风险。随着CORS(跨源资源共享)等更现代的解决方案的出现,JSONP的使用正在逐渐减少。

Angular中HttpClient模块也提供了JSONP的支持,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
selector: 'app-jsonp',
templateUrl: './jsonp.component.html',
styleUrls: ['./jsonp.component.less']
})
export class JsonpComponent implements OnInit {
data: any;
constructor(private http: HttpClient) {}

ngOnInit(): void {
this.http.jsonp('http://localhost:3000/users', 'callback')
.subscribe((data: any) => {
this.data = data;
});
}
}

前端使用反向代理

这种方法一般是开发阶段使用的,因为本质上,前端是无法解决跨域问题的,只能通过后端来解决。

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.jsonproject.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