跨域请求问题
跨域是指发送与当前服务的域名(或端口)不一致的请求。跨域问题的产生原因是浏览器不允许JS请求对域名不同或端口不同的服务发起请求(同源策略)。
演示跨域问题:
-
编辑一个
html
页面:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.staticfile.org/jquery/3.7.0/jquery.min.js"></script> </head> <body> <button id="btnGET">GET</button> <button id="btnPOST">POST</button> <script> $(function () { // 测试GET接口 $('#btnGET').on('click', () => { $.ajax({ type: 'GET', url: 'http://localhost/get', data: { name: 'Zhangsan', age: 20 }, success: (res) => { console.log(res); } }) }); // 测试POST接口 $('#btnPOST').on('click', () => { $.ajax({ type: 'POST', url: 'http://localhost/post', data: { name: '人间失格', author: '太宰治' }, success: (res) => { console.log(res); } }) }); }) </script> </body> </html>
-
创建Express Server:
const express = require('express'); const app = express() app.use(express.json()) app.get('/get', (req, res) => { res.send({ status: 0, msg: '请求成功!' }) }) app.post('/post', (req, res) => { res.send({ status: 0, msg: '请求成功!', data: req.body }) }) app.listen(80, () => { console.log('Server running at http://127.0.0.1/'); })
只要页面打开的协议、地址或端口与服务器不同,浏览器就会阻止该请求的发送。解决方案有CORS和JSONP等,推荐使用CORS,因为JSONP仅支持GET请求。
在Express中,可以通过使用cors
这个第三方中间件来解决跨域问题:
-
安装
cors
:npm i cors
-
导入
cors
:const cors = request('cors')
-
注册
cors
中间件:app.use(cors())
注:
cors
中间件要在所有路由之前注册。
完整示例如下:
const express = require('express');
const app = express()
// 导入并注册 cors 中间件
const cors = require('cors');
app.use(cors())
app.use(express.json())
app.get('/get', (req, res) => {
res.send({
status: 0,
msg: 'GET请求成功!',
data: req.query
})
})
app.post('/post', (req, res) => {
res.send({
status: 0,
msg: 'POST请求成功!',
data: req.body
})
})
app.listen(80, () => {
console.log('Server running at http://127.0.0.1/');
})
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
:Access-Control-Allow-Origin: <origin> | *
其中,
origin
参数的值指定了允许访问该资源的外域URL;如果要允许所有网站进行跨域请求,可以使用*
符号。例如在服务端设置仅允许
https://blog.linner.asia
进行跨域请求:res.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
请求头:res.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 请求(多个请求之间使用
,
分隔):res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
-
使用
*
符号允许所有的 HTTP 请求:
res.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请求:
<button id="btnDELETE">DELETE</button>
// 测试DELETE接口(演示预检请求) $('#btnDELETE').on('click', function () { $.ajax({ type: 'DELETE', url: 'http://127.0.0.1/delete', data: { id: '1' }, success: function (res) { console.log(res); } }) });
-
在Express Server中添加以下接口:
app.delete('/delete', (req, res) => { res.send({ status: 0, msg: 'DELETE请求成功!', data: req.body }) })
在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接口的步骤:
-
获取客户端发送过来的回调函数的名字。
const funcName = req.query.callback
-
得到要通过JSONP形式发送给客户端的响应数据。
-
根据前两步得到的数据,拼接出一个函数调用的字符串。
const data = /* get response data... */ // 拼接函数调用的字符串 const scriptStr = `${funcName}(${JSON.stringify(data)})`
-
把上一步拼接得到的字符串,响应给客户端的
<script>
标签进行解析执行。res.send(scriptStr)
客户端进行JSONP请求的示例:
<button id="btnJSONP">JSONP</button>
$('#btnJSONP').on('click', function () {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/jsonp',
dataType: 'jsonp', // 表示发起JSONP请求
success: function (res) {
console.log(res);
}
})
});
完整的JSONP接口实现示例如下:
app.use('/jsonp', (req, res) => {
// TODO: 定义 JSONP 接口具体的实现过程
// 获取回调函数名称
const funcName = req.query.callback
// 获取响应数据
const data = {
name: 'Zhangsan',
age: 22
}
// 拼接函数调用的字符串
const scriptStr = `${funcName}(${JSON.stringify(data)})`
// 将函数调用字符串响应给客户端的 <script> 标签进行解析执行
res.send(scriptStr)
})
// 如果同时启用了 CORS 和 JSONP,CORS中间件要放在JSONP接口后面,否则JSONP接口也会以CORS形式进行相应
评论