跨域请求问题
跨域是指发送与当前服务的域名(或端口)不一致的请求。跨域问题的产生原因是浏览器不允许JS请求对域名不同或端口不同的服务发起请求(同源策略)。
演示跨域问题:
-
编辑一个
html页面:1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 <script src="https://cdn.staticfile.org/jquery/3.7.0/jquery.min.js"></script> 8</head> 9<body> 10 <button id="btnGET">GET</button> 11 <button id="btnPOST">POST</button> 12 13 <script> 14 $(function () { 15 // 测试GET接口 16 $('#btnGET').on('click', () => { 17 $.ajax({ 18 type: 'GET', 19 url: 'http://localhost/get', 20 data: { 21 name: 'Zhangsan', 22 age: 20 23 }, 24 success: (res) => { 25 console.log(res); 26 } 27 }) 28 }); 29 // 测试POST接口 30 $('#btnPOST').on('click', () => { 31 $.ajax({ 32 type: 'POST', 33 url: 'http://localhost/post', 34 data: { 35 name: '人间失格', 36 author: '太宰治' 37 }, 38 success: (res) => { 39 console.log(res); 40 } 41 }) 42 }); 43 }) 44 </script> 45</body> 46</html> -
创建Express Server:
1const express = require('express'); 2const app = express() 3 4app.use(express.json()) 5 6app.get('/get', (req, res) => { 7 res.send({ 8 status: 0, 9 msg: '请求成功!' 10 }) 11}) 12 13app.post('/post', (req, res) => { 14 res.send({ 15 status: 0, 16 msg: '请求成功!', 17 data: req.body 18 }) 19}) 20 21app.listen(80, () => { 22 console.log('Server running at http://127.0.0.1/'); 23})
只要页面打开的协议、地址或端口与服务器不同,浏览器就会阻止该请求的发送。解决方案有CORS和JSONP等,推荐使用CORS,因为JSONP仅支持GET请求。
在Express中,可以通过使用cors这个第三方中间件来解决跨域问题:
-
安装
cors:1npm i cors -
导入
cors:1const cors = request('cors') -
注册
cors中间件:1app.use(cors())注:
cors中间件要在所有路由之前注册。
完整示例如下:
1const express = require('express');
2const app = express()
3
4// 导入并注册 cors 中间件
5const cors = require('cors');
6app.use(cors())
7
8app.use(express.json())
9
10app.get('/get', (req, res) => {
11 res.send({
12 status: 0,
13 msg: 'GET请求成功!',
14 data: req.query
15 })
16})
17
18app.post('/post', (req, res) => {
19 res.send({
20 status: 0,
21 msg: 'POST请求成功!',
22 data: req.body
23 })
24})
25
26app.listen(80, () => {
27 console.log('Server running at http://127.0.0.1/');
28})
CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源。
浏览器的同源安全策路默认会阻止网页“跨域”获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。相当于服务器告诉浏览器我同意其它“域”来使用我的接口。
CORS的注意事项:
- CORS主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。
- CORS在浏览器中有兼容性。只有支持XMLHttpRequest Level2的浏览器,才能正常访问开启了CORS的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
与CORS相关的响应头,基本上都是以Access-Control-Allow-开头。常见的CORS相关响应头有:
-
Access-Control-Allow-Origin:1Access-Control-Allow-Origin: <origin> | *其中,
origin参数的值指定了允许访问该资源的外域URL;如果要允许所有网站进行跨域请求,可以使用*符号。例如在服务端设置仅允许
https://blog.linner.asia进行跨域请求:1res.setHeader('Access-Control-Allow-Origin', 'https://blog.linner.asia') -
Access-Control-Allow-Headers:默认情况下,CORS仅支持客户端向服务器发送如下的9种请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(值仅限于text/plain、multipart/form-data、application/x-wwnw-form-urlencoded三者之)。如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过
Access-Control-Allow-Headers对额外的请求头进行声明;否则这次请求将会失败。例如在服务器中通过设置
Access-Control-Allow-Headers来允许客户端发送Content-Type和XCustom-Header请求头:1res.setHeader(`Access-Control-Allow-Headers`, 'Content-Type, XCustom-Header')注:
res.setHeader()中设置Access-Control-Allow-Headers时,多个请求头之间使用,分隔。 -
Access-Control-Allow-Methods:默认情况下,CORS仅支持客户端发起GET、POST、HEAD请求。如果客户端希望通过PUT、DELETE等方式请求服务器的资源,则需要在服务器端。通过
Access-Control-Allow-Methods来指明实际请求所允许使用的HTTP方法,-
只允许 POST、GET、DELETE、HEAD 请求(多个请求之间使用
,分隔):1res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD') -
使用
*符号允许所有的 HTTP 请求:
1res.setHeader('Access-Control-Allow-Methods', '*') -
客户端在请求CORS接口时,根据请求方式和请求头的不同,可以将CORS的请求分为两大类,分别是:
-
简单请求:
同时满足以下两大条件的请求,就属于简单请求:
-
请求方式必须是 GET、POST、HEAD 之一。
-
HTTP头部信息不超过以下几种字段:
无自定义头部字段、
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(值仅限于text/plain、multipart/form-data、application/x-wwnw-form-urlencoded三者之)。
特点:客户端与服务器之间只会发生一次请求。
-
-
预检请求:
只要符合以下任一条件的请求,都需要进行预检请求:
- 请求方式为 GET、POST、HEAD 之外的请求类型。
- 请求头中包含了自定义头部字段。
- 发送的请求数据包含
application/json格式的数据。
在浏览器与服务器正式通信之前,浏览器会先发送
OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求被称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,井且携带真实数据。特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求。
演示:
-
在HTML中添加以下按钮和Ajax请求:
1<button id="btnDELETE">DELETE</button>1// 测试DELETE接口(演示预检请求) 2$('#btnDELETE').on('click', function () { 3 $.ajax({ 4 type: 'DELETE', 5 url: 'http://127.0.0.1/delete', 6 data: { 7 id: '1' 8 }, 9 success: function (res) { 10 console.log(res); 11 } 12 }) 13}); -
在Express Server中添加以下接口:
1app.delete('/delete', (req, res) => { 2 res.send({ 3 status: 0, 4 msg: 'DELETE请求成功!', 5 data: req.body 6 }) 7})
在Edge、Firefox等能显示预检请求的浏览器(Google Chrome需要在chrome://flags/#out-of-blink-cors中开启显示预检请求)中打开HTML页面,点击DELETE按钮发起请求。然后在开发者工具的网络监测中可以看到发送了一条大小为0的OPTION预检请求。
JSONP 接口
浏览器端通过<script>标签的src属性,请求服务器上的数据。同时,服务器返回一个函数的调用。这种请求数据的方式叫做JSONP。
JSONP的特点:
- JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象。
- JSONP仅支持GET请求,不支持POST、PUT、DELETE等请求。
实现JSONP接口的步骤:
-
获取客户端发送过来的回调函数的名字。
1const funcName = req.query.callback -
得到要通过JSONP形式发送给客户端的响应数据。
-
根据前两步得到的数据,拼接出一个函数调用的字符串。
1const data = /* get response data... */ 2// 拼接函数调用的字符串 3const scriptStr = `${funcName}(${JSON.stringify(data)})` -
把上一步拼接得到的字符串,响应给客户端的
<script>标签进行解析执行。1res.send(scriptStr)
客户端进行JSONP请求的示例:
1<button id="btnJSONP">JSONP</button>
1$('#btnJSONP').on('click', function () {
2 $.ajax({
3 type: 'GET',
4 url: 'http://127.0.0.1/jsonp',
5 dataType: 'jsonp', // 表示发起JSONP请求
6 success: function (res) {
7 console.log(res);
8 }
9 })
10});
完整的JSONP接口实现示例如下:
1app.use('/jsonp', (req, res) => {
2 // TODO: 定义 JSONP 接口具体的实现过程
3 // 获取回调函数名称
4 const funcName = req.query.callback
5 // 获取响应数据
6 const data = {
7 name: 'Zhangsan',
8 age: 22
9 }
10 // 拼接函数调用的字符串
11 const scriptStr = `${funcName}(${JSON.stringify(data)})`
12 // 将函数调用字符串响应给客户端的 <script> 标签进行解析执行
13 res.send(scriptStr)
14})
15// 如果同时启用了 CORS 和 JSONP,CORS中间件要放在JSONP接口后面,否则JSONP接口也会以CORS形式进行相应
评论