什么是跨域 相信很多做前端开发的同学都在浏览器控制台遇到过如下错误。
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
vs localhost
1 http://localhost:4200 和 http://remotehost:3000
协议不同http
vs https
1 http://localhost:3000 和 https://localhost:3000
端口不同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请求,询问服务器是否允许跨域请求,如果服务器允许,浏览器才会发送真正的请求。
简单请求: 满足以下条件的请求是简单请求。
请求方法是以下三种方法之一:
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 2 3 4 5 Request URL : http :**Request Method **: OPTIONS Status Code : 200 OK Remote Address : 10.10 .143 .144 :9898 Referrer Policy : strict-origin-when-cross-origin
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
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, uuidAccess -Control -Allow -Methods : PUT ,DELETE ,GET ,POST ,OPTIONS Access -Control -Allow -Origin : http :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-storeConnection : keep-aliveContent -Length : 0 Date : Tue , 04 Jan 2022 03 :08 :03 GMT Pragma : no-cacheServer : nginx/1.17 .5 Strict -Transport -Security : max-age=8995000 ; includeSubdomainsVary : 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。
创建一个Express项目并安装cors1 2 3 npm 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 22 const express = require ('express' );const cors = require ('cors' );const app = express ();const port = 3000 ;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); });
启动项目
新建一个前端项目(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; }); } }
记得在app.module.ts中启用HttpClientModule1 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实现跨域的基本步骤:
创建<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 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.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