关于处理跨域请求中的options请求验证,使用node服务测试

发表于 node 分类,标签:

版本信息

作者时间主要变更内容链接
吴惟刚2022/02/15options请求的原有http://wuweigang.com/?id=361

概述

目前跨域的处理方式主要是nginx反向代理,但是也存在直接后端允许跨域的情况,这样就不需要额外增加一个nginx代理层了,但是后端服务器允许了跨域,我们还是会遇到一些特殊的情况, 那就是部分请求会提前发送 options请求做验证,然后options请求未通过,就导致请求不到数据,下面我们分析下options请求的具体原因和解决方案。

HTTP的请求方法

首先我们了解一下,http请求方式到底有多少种

序号请求方式请求描述
1【GET】获取数据 ( 最常用)
2【POST】新增数据( 最常用)
3【PUT】更新数据( 最常用)
4【DELETE】删除数据 ( 最常用)
5【PATCH】方法是对 PUT/POST 方法的补充,用于对资源进行部分的修改,是非幂等的。
6【OPTIONS】询问服务器是否支持该请求
7【HEAD】获得报文首部
8【MOVE】请求服务器将指定的页面移至另一个网络地址。
9【COPY】请求服务器将指定的页面移至另一个网络地址。
10【LINK】请求服务器建立链接关系。
11【UNLINK】断开链接关系。
12【WRAPPED】允许客户端发送经过封装的请求。 (错误请求(Bad Request))
13【Extension-mothed】在不改动协议的前提下,可增加另外的方法。
14【CONNECT】要求用隧道协议连接代理,HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。 (经过测试ajax method不支持,无法到后端)
15【TRACE】追踪路径,回显服务器收到的请求,主要用于测试或诊断。 (经过测试ajax method不支持,无法到后端)

上述请求中,get,post,put,delete为最常用的请求

跨域请求测试

自己写一个ajax,后端用node起个服务,经过测试, 上述http请求,WRAPPEDExtension-mothedCONNECTTRACE 无法跨域成功.

CONNECTTRACE 直接显示不支持, 错误信息如下:

1
DOMException: Failed to execute 'open' on 'XMLHttpRequest': 'TRACE' HTTP method is unsupported.

WRAPPEDExtension-mothed 无法设置成功,

1
test-cross-xhr.html:1 Access to XMLHttpRequest at 'http://172.16.192.153:1234/api?ID=12345' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

其他请求方式均可测试成功, 所以理论上,我们应该有11种请求方式。下面具体分下这11种请求发送options请求的条件和解决方案。

什么情况下,http请求会发送options请求?

options 请求主要作用就是验证是否有访问权限服务器接口,有的话,才会发送真实请求,否则请求数据失败。

所以options请求称之为 认证请求

1、前提条件,必须是跨域请求

请求必须是跨域的才会发送options请求

2、默认只请求,不设置头部信息, 是否发送options请求

在不设置任何header请求下,利用axios发送一个非常简单的ajax,只改变 method,

1
2
3
4
5
6
// 利用axios发送请求
axios({
       method: 'get', // post 、head,  put
       // url: 'http://fd.thunisoft.com:1234/api?bane=5&aaa=8888',
       url: 'http://172.16.192.153:1234/api',
 })

各请求方式,是否提前发送options请求,测试结果如下:

序号请求方式是否发送options请求
1【GET】
2【POST】
3【PUT】发送 options 请求
4【DELETE】发送 options 请求
5【PATCH】发送 options 请求
6【OPTIONS】发送 options 请求
7【HEAD】
8【MOVE】发送 options 请求
9【COPY】发送 options 请求
10【LINK】发送 options 请求
11【UNLINK】发送 options 请求
12【WRAPPED】发送 options 请求
13【Extension-mothed】发送 options 请求
14【CONNECT】无法到后端
15【TRACE】无法到后端

可以得出结论, get, post 和 head为简单请求,不设置头信息的情况下,不会发送options请求

3、设置头部信息,所有请求都将先发送options请求

如果发送ajax时, 设置了ajax的header, 那么所有请求包含简单请求get, post, head都会先发送options请求验证

1
2
3
4
5
6
7
8
9
// 利用axios发送请求
axios({
       method: 'get', // post 、head,  put
       // url: 'http://fd.thunisoft.com:1234/api?bane=5&aaa=8888',
       url: 'http://172.16.192.153:1234/api',
       headers: {
        'Content-Type':'application/text', // 可修改,报错,(后端需要修改)
       }
 })

options请求验证失败的原因,以及解决方案

1. 后端服务器未允许跨域

如: 前端为http://www.baidu.com请求后端为 http://home.thunisoft.com/api. 那么这个就是跨域的,但是后端未设置运行跨域。

以node服务为例,后端需要设置

1
2
// 设置允许跨域的域名,*代表允许任意域名跨域
response.setHeader('Access-Control-Allow-Origin',  '*');

2. 后端服务器未允许某种请求方式

如:前端请求用的 delete请求,后端后端仅设置了允许 get和post请求。

以node服务为例,后端需要设置以下请求方式,我这里是把15种请求方式全部允许了,项目根据实际情况设置

1
2
// 4.跨域允许的请求方式
  response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS,COMMON,HEAD, MOVE, COPY, LINK, UNLINK, WRAPPED, Extension-mothed, CONNECT, TRACE');

3.前端发送的ajax设置header字段,后端服务器未允许

以node服务为例,后端需要对应加上前端发送的ajax header字段

1
2
// 3.3 X-Requested-With 对应 XMLHttpRequest ()
 response.setHeader('Access-Control-Allow-Headers', 'Cache-Control,Content-Type, pragma, Authorization, X-Requested-With,  wuwg-define');

  • Cache-Control 缓存设置

  • Content-Type 内容类型,前端一般不要设置, 让浏览器自行识别

  • pragma 缓存设置

  • Authorization 对应密码验证,

  • X-Requested-With XMLHttpRequet ajax请求

  • wuwg-define 自定义的头

4.前端发送了cookie信息,后端服务器未设置

axios 前端代码 withCredentials :true

1
2
3
4
5
6
axios({
     method: 'get',
     url: 'http://172.16.192.153:1234/api?bane=5&aaa=8888',
     //`withCredentails`选项表明了是否是跨域请求
     withCredentials:true,
})

报以下错

1
Access to XMLHttpRequest at 'http://172.16.192.153:1234/api?ID=12345' from origin 'http://localhost:63342' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is 'false' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

以node服务为例,后端需要设置

1
2
3
4
5
 const origin = request.headers.origin;  
// 1.设置允许跨域的域名
response.setHeader('Access-Control-Allow-Origin',  origin || '*');  
// 2.认证携带cookie, 注意, 如果跨域的话,Access-Control-Allow-Origin 字段必须指定域名,不能为*, 请看上面的写法
response.setHeader('Access-Control-Allow-Credentials', true);

特别注意,上述代码设置后还需要设置 Access-Control-Allow-Origin 值, 它的值不能为 *, 否则还是不能访问成功

建议

虽然已经找到options请求问题的原因和解决方案,但是前端在使用ajax时应尽量避免上述问题, 有时候后端配置或者运维配置,就会导致前端请求不到数据,所以能避免就避免,总结前端经验

  1. 尽量使用简单请求get,post

  2. 尽量不要修改headers字段信息,尤其是新增自定义header字段信息, 增加一个后端就必须多配一个,否则options验证就通不过

  3. 最重要的一点,一定要知晓原理,看到浏览器报错应该知晓什么原因引起的。

实例代码

注释部分,大家可以自行按照上面的步骤依次放开,测试上述结论的正确性

1. 前端代码,依赖 axios库

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>test axios  cross</title>
</head>
<body>
<h1>测试axios  发送的ajax 拿  cross 的包!</h1>
<script src="js/axios.js"></script>
<script>
   //  返回shuju
   axios({
       method:'GET', // 不发送 options 请求
       // method:'POST', // 不发送 options 请求
       // method:'PUT', //  发送 options 请求
       // method:'DELETE', //  发送 options 请求
       // method:'PATCH', //  发送 options 请求
       // method:'OPTIONS', //  发送 options 请求
       // method:'HEAD', // 不发送 options 请求
       // method:'MOVE', //  发送 options 请求
       // method:'COPY', //  发送 options 请求
       // method:'LINK', //  发送 options 请求
       // method:'UNLINK', //  发送 options 请求
       // method:'WRAPPED', //  发送 options 请求
       // method:'Extension-mothed', //  发送 options 请求
       // method:'CONNECT',  // 直接无法到后端
       // method:'TRACE',  // 直接无法到后端
       params:{
           ID:12345
       },
       data:{
           test:'data'
       },
       url: 'http://localhost:1234/api',
       // `withCredentails`选项表明了是否是跨域请求
       withCredentials: true,
       auth: {
           username:'wuwg',
           password: '1'
       },
       // default
       responseType:'json',
       // headers: {
       //
       //     'Accept-Encoding':'deflate', // 不可修改
       //     'Connection':'keep-alive', // 不可修改
       //     'Content-Length': 1000, // 不可修改
       //     'Host': 'localhost:1234', // 不可修改
       //     'Origin': 'http://www.baidu.com', // 不可修改
       //     'Referer': 'http://www.baidu.com', // 不可修改
       //     'User-Agent': 'Mozilla/5.0', // 不可修改
       //
       //     'Accept':'text/plain', // 可修改, 不报错(后端不需要修改)
       //     'Accept-Language':'zh-CN', // 可修改,不报错(后端不需要修改)
       //
       //
       //     'Content-Type':'application/text', // 可修改,报错,(后端需要修改)
       //     'pragma':'private', //  可修改,报错,(后端需要修改)
       //     'Cache-Control': 'cache', //  可修改,报错,(后端需要修改)
       //     'X-Requested-With':'XMLHttpRequest', // 可修改,报错,(后端需要修改)
       //
       //     'wuwg-define':'wuwg' // 自定义头
       // }
   }).then((response) => {
       debugger
       console.log(response);
   }, (data) => {
       debugger
       console.log(data)
   });

</script>
</body>
</html>

2. 后端node服务器代码

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const http = require('http');
const url = require('url');
// 请求计数
let  i = 0;
// 创建服务
http.createServer(function(request, response) {
   i++;
   // console.log(request)
   const origin = request.headers.origin;
   // 1.设置允许跨域的域名,*代表允许任意域名跨域
   console.log(origin)
   // response.setHeader('Access-Control-Allow-Origin',  '*');
   response.setHeader('Access-Control-Allow-Origin',  origin || '*');
   // 2.认证携带cookie, 注意, 如果跨域的话,Access-Control-Allow-Origin 字段必须指定域名,不能为*, 请看上面的写法
   response.setHeader('Access-Control-Allow-Credentials', true);
   // 3.允许的header类型
   // 3.1 一般默认头部包含以下:  Accept,Accept-Encoding,Accept-Language,Cache-Control,Connection,Content-Length,Host,Origin,Pragma,Referer,User-Agent
   // 如果需要修改默认头部的值,最好在 Access-Control-Allow-Headers 中添加上
   // 3.2 Authorization 对应密码验证
   // 3.3 X-Requested-With 对应 XMLHttpRequest ()
    response.setHeader('Access-Control-Allow-Headers', 'Cache-Control,Content-Type, pragma, Authorization, X-Requested-With,  wuwg-define');
   // 4.跨域允许的请求方式
   response.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS,COMMON,HEAD, MOVE, COPY, LINK, UNLINK, WRAPPED, Extension-mothed, CONNECT, TRACE');
   // 5. 其他头部信息
   response.setHeader('header-wuwg-define', 'test');
   const  method = request.method;
   console.log('第'+ i +'次走!' );
   console.log('请求方式为:'+ request.method);
   console.log('\n');
   //设置请求头
   response.writeHead(200, {
       'Content-Type': 'text/plain;charset=utf-8'
   });
   // 简单请求;
   if ( /(get|head)/.test(method)) {
       response.end( method +'【简单请求】成功');
   }
   else if (method === 'post') {
       let body = '';
       request.on('data',function (chunk) {
           body += chunk;  //一定要使用+=,如果body=chunk,因为请求favicon.ico,body会等于{}
       });
       // //在end事件触发后,然后向客户端返回。
       request.on('end',function () {
         response.end( 'post【简单请求】成功');
       });
       request.on('error',function () {
           console.log('数据获取失败')
       });
   }
   else {
       // 其他请求
     response.end( method +'【非简单请求】成功');
   }

}).listen(1234)
// 启动一个服务器
console.log('server run http://localhost:1234');

附录一个简单原生ajax方法,供大家测试使用

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* 原生ajax
* @author  wuwg
* @time    20220215
* @version 1.0.0
* @param config
* @return {Promise<unknown>}
*/
function ajax(config) {
   return new Promise((resolve, reject) => {
       let request = new XMLHttpRequest();
       const method = config.method;
       let url = config.url;
       const params = config.params;
       // 1.拼接get 参数
       if (params) {
           Object.entries(params).forEach((item, index) => {
               const _dataItem = `${index === 0 ? '?' : '&'}${item[0]}=${item[1]}`;
               url += _dataItem;
           });
       }
       // 2.打开请求
       request.open(method, url, true);
       // 3. headers 信息
       let headers = {};
       // 3.1 基础认证
       if (config.auth) {
           const username = config.auth.username || '';
           const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
           // 添加认证
           headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;
       }
       // 3.2 合并其他头
       if (config.headers) {
           headers = Object.assign(headers, config.headers);
       }
       // 3.3 给request设置头部
       Object.entries(headers).forEach((item) => {
           // 设置头部
           request.setRequestHeader(item[0], item[1]);
       });
       // 4. 设置其他属性
       // 4.1添加 cookie 权限
       request.withCredentials = Boolean(config.withCredentials);
       // 4.2 设置超时
       request.timeout = config.timeout;
       // 5. 设置函数
       // 5.1 成功函数
       request.onreadystatechange = function handleLoad() {
           if (!request || request.readyState !== 4) {
               return;
           }
           // Prepare the response
           const responseHeaders = 'getAllResponseHeaders' in request ? request.getAllResponseHeaders() : null;
           const responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
           // 响应数据
           const response = {
               data: responseData,
               status: request.status,
               statusText: request.statusText,
               headers: responseHeaders,
               config: config,
               request: request
           };
           if (response.status >= 200 && response.status < 300) {
               resolve(response);
           } else {
               reject(response);
           }
           // Clean up request
           request = null;
       };
       // 5.2 取消函数
       request.onabort = function handleAbort() {
           if (!request) {
               return;
           }

           reject({
               message:'Request aborted',
               config,
               request
           });

           // Clean up request
           request = null;
       };
       // 5.3 错误函数
       request.onerror = function handleError() {
           reject({
               message:'Network Error',
               config,
               request
           });
           // Clean up request
           request = null;
       };
       // 5.1 超时函数
       request.ontimeout = function handleTimeout() {
           reject({
               message:'timeout Error',
               config,
               request
           });
           // Clean up request
           request = null;
       };

       // 6. 设置取消函数
       config.abort = request.abort;
       // 7. 发送请求
       const requestData = config.data || null;
       request.send(requestData);
   });
}

使用

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<script src="./js/xhr.js"></script>
<script>
//  返回shuju
   ajax({
       method:'GET', // 不发送 options 请求
       // method:'POST', // 不发送 options 请求
       // method:'PUT', //  发送 options 请求
       // method:'DELETE', //  发送 options 请求
       // method:'PATCH', //  发送 options 请求
       // method:'OPTIONS', //  发送 options 请求
       // method:'HEAD', // 不发送 options 请求
       // method:'MOVE', //  发送 options 请求
       // method:'COPY', //  发送 options 请求
       // method:'LINK', //  发送 options 请求
       // method:'UNLINK', //  发送 options 请求
       // method:'WRAPPED', //  发送 options 请求
       // method:'Extension-mothed', //  发送 options 请求
       // method:'CONNECT',  // 直接无法到后端
       // method:'TRACE',  // 直接无法到后端
       params:{
           ID:12345
       },
       data:{
           test:'data'
       },
       url: 'http://localhost:1234/api',
       // `withCredentails`选项表明了是否是跨域请求
       withCredentials: true,
       auth: {
           username:'wuwg',
           password: '1'
       },
       // default
       responseType:'json',
       // headers: {
       //
       //     'Accept-Encoding':'deflate', // 不可修改
       //     'Connection':'keep-alive', // 不可修改
       //     'Content-Length': 1000, // 不可修改
       //     'Host': 'localhost:1234', // 不可修改
       //     'Origin': 'http://www.baidu.com', // 不可修改
       //     'Referer': 'http://www.baidu.com', // 不可修改
       //     'User-Agent': 'Mozilla/5.0', // 不可修改
       //
       //     'Accept':'text/plain', // 可修改, 不报错(后端不需要修改)
       //     'Accept-Language':'zh-CN', // 可修改,不报错(后端不需要修改)
       //
       //
       //     'Content-Type':'application/text', // 可修改,报错,(后端需要修改)
       //     'pragma':'private', //  可修改,报错,(后端需要修改)
       //     'Cache-Control': 'cache', //  可修改,报错,(后端需要修改)
       //     'X-Requested-With':'XMLHttpRequest', // 可修改,报错,(后端需要修改)
       //
       //     'wuwg-define':'wuwg' // 自定义头
       // }
   }).then((data) => {
           debugger
   }, (data)=> {
       console.log(data)
       debugger
   })
</script>


0 篇评论

发表我的评论