跨域及解决方案
# 跨域及解决方案
# 什么是跨域
有跨域就会有同域,也就是我们常说的同源策略 (opens new window) , MDN 上对于同源策略的解释为
同源策略
是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
如果两个 URL 的 protocol、port (如果有指定的话) 和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为 “协议 / 主机 / 端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重 / 三重 / 四重 / 五重 / 等的通用形式)。
是什么意思呢,简单来说就是同源策略是一种约束,作用是为了安全性考虑,它用来防止一些例如 XSS、CSRF 等攻击。
同源是指 协议 + 域名 + 端口
三者相同,子域名不同也认为不同源
# 同源策略会限制哪些内容?
LocalStorage
、Cookie
、IndexDB
等存储相关,只能同域访问DOM
元素也存在同源策略,例如<iframe></iframe>
Ajax
请求的返回结果,Ajax
跨域访问拦截的不是请求,是返回的结果,浏览器会认为不安全,会拦截
# 在 DOM 元素中存在以下标签,是允许跨域访问资源的
<img src="https://www.baidu.com/test.png" />
<link href="https://www.google.com/test.css" />
<script src="https://www.google.com/test.js"></script>
# 针对于 Ajax
请求的跨域问题
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了, 因为 Ajax
请求会读取返回内容,因此浏览器不允许你这么做(一个域名的 JS ,在未经允许的情况下,不得读取另一个域名的内容。但浏览器并不阻止你向另一个域名发送请求。)。但是 form
表单却不存在 跨域的问题,是因为 form
表单再提交后,并不能获取到返回的内容,所以浏览器认为这是安全的。
Ajax
和 form
表单提交,都可以携带 cookie
,所以 form
表单提交 和 Ajax
并不能阻止 CSRF
攻击,因为请求已经发出去了,通过 cookie
的新属性 SameSite
可以避免这个问题
# 跨域解决方案
# jsonp
# jsonp 原理
JSONP
的原理非常简单,就是利用 HTML
标签的 src
属性可以跨域访问资源的特性,通过 script
标签来执行跨域的 JS
代码,通过这些代码,来实现前端跨域请求数据
# jsonp 的实现
首先声明一个回调函数,函数名当做参数,传递给跨域请求的服务器
<script> function callback(data) { console.log(data) } </script>
1
2
3
4
5创建一个
script
标签,script
标签的src
为需要跨域请求的服务器地址,然后拼接回调函数参数,回调函数的名称按照约定来书写,以百度搜索举例我们在百度搜索时,百度搜索存在根据用户输入自动识别显示匹配列表,这个过程就是一个
jsonp
的请求过程在
Network
中,搜索不属于XHR
请求,它是一个jsonp
的请求,我们以搜索aab
为例,得到搜索的地址如下https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=33358,33261,33344,31660,33692,33595,33759,33675,33714,26350&wd=aab&req=2&csor=3&pwd=a&cb=jQuery110207690587662328763_1616507377096&_=1616507377102
1这里
wd=aab
是我们搜索的关键字,cb=jQuery110207690587662328763_1616507377096
则是jsonp
的回调函数我们拿到关键的信息,简化
jsonp
地址,得到如下地址https://www.baidu.com/sugrec?prod=pc&wd=aab&cb=callback
1访问结果如下
// https://www.baidu.com/sugrec?prod=pc&wd=aab&cb=show show({ "q": "aab", "p": false, "g": [ { "type": "sug", "sa": "s_1", "q": "aabc式词语大全" }, { "type": "sug", "sa": "s_2", "q": "aab式的词语" }, { "type": "sug", "sa": "s_3", "q": "aabb式的词语" }, { "type": "sug", "sa": "s_7", "q": "aabc式词语大全三年级下册" }, { "type": "sug", "sa": "s_8", "q": "aabc式词语大全 成语二年级" }, { "type": "sug", "sa": "s_9", "q": "aabc式词语四字" }, { "type": "sug", "sa": "s_10", "q": "aabc成语四字" } ], "slid": "7841404972524760491", "queryid": "0x644446e751edab" })
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那么此时,我们可以创建一个
script
标签,配合第一步创建的函数,来实现jsonp
,拿到返回的数据<script> function callback(data) { console.log(data) } </script> <script src="https://www.baidu.com/sugrec?prod=pc&wd=aab&cb=callback"></script>
1
2
3
4
5
6至此就实现了一个简单的
jsonp
请求
# jsonp 的封装
在日常开发中,如果每次都需要这么写,就显得很繁琐,我们可以根据实现思路,封装一个 jsonp
请求
function jsonp({ url, params, cb }) {
// 处理参数拼接
function handlerParams(query) {
return Object.keys(query).reduce((result, k, i) => (result = result + (i < 1 ? '' : `&`) + `${k}=${query[k]}`, result), '?')
}
return new Promise((resolve, reject) => {
// 1. 首先创建 script 标签
const script = document.createElement('script')
// 2. 创建一个挂载到 window 上的函数,方便访问,在函数里将数据抛出去,然后降创建的 script 标签删除掉
window[cb] = function(data) {
resolve(data);
document.body.removeChild(script)
}
// 3. 将回调函数名拼接到最后
params = {...params, cb}
// 4. 处理参数后拼接url
script.src = url + handlerParams(params)
// 5. 将 script 标签添加到 body 中
document.body.appendChild(script)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 其他 jsonp 的实现
/**
* Module dependencies
*/
var debug = require('debug')('jsonp');
/**
* Module exports.
*/
module.exports = jsonp;
/**
* Callback index.
*/
var count = 0;
/**
* Noop function.
*/
function noop(){}
/**
* JSONP handler
*
* Options:
* - param {String} qs parameter (`callback`)
* - prefix {String} qs parameter (`__jp`)
* - name {String} qs parameter (`prefix` + incr)
* - timeout {Number} how long after a timeout error is emitted (`60000`)
*
* @param {String} url
* @param {Object|Function} optional options / callback
* @param {Function} optional callback
*/
function jsonp(url, opts, fn){
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
if (!opts) opts = {};
var prefix = opts.prefix || '__jp';
// use the callback name that was passed if one was provided.
// otherwise generate a unique name by incrementing our counter.
var id = opts.name || (prefix + (count++));
var param = opts.param || 'callback';
var timeout = null != opts.timeout ? opts.timeout : 60000;
var enc = encodeURIComponent;
var target = document.getElementsByTagName('script')[0] || document.head;
var script;
var timer;
if (timeout) {
timer = setTimeout(function(){
cleanup();
if (fn) fn(new Error('Timeout'));
}, timeout);
}
function cleanup(){
if (script.parentNode) script.parentNode.removeChild(script);
window[id] = noop;
if (timer) clearTimeout(timer);
}
function cancel(){
if (window[id]) {
cleanup();
}
}
window[id] = function(data){
debug('jsonp got', data);
cleanup();
if (fn) fn(null, data);
};
// add qs component
url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id);
url = url.replace('?&', '?');
debug('jsonp req "%s"', url);
// create script
script = document.createElement('script');
script.src = url;
target.parentNode.insertBefore(script, target);
return cancel;
}
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
# jsonp 限制
jsonp 只支持 get 请求,对于其他请求不支持。优点是兼容性好。
# CORS
# CORS (opens new window) 是什么
CORS
跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),这样浏览器可以访问加载这些资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的 "预检" 请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
原理
跨源资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
注意
IE 10 提供了对规范的完整支持,但在较早版本(8 和 9)中,CORS 机制是借由 XDomainRequest 对象完成的
# CORS 发送请求的情况
不会触发 CORS 预检的请求,也就是简单请求,若请求满足所有下述条件,则该请求可视为 简单请求 (opens new window) :
- 使用下列方法之一
GET
HEAD
POST
Content-Type
的值仅限于下列三者之一text/plain
multipart/form-data
application/x-www-form-urlencoded
- 请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问
还有其他情况可参考 简单请求 (opens new window) , 这里列出了一些比较常用的
- 使用下列方法之一
会触发 CORS 预检的请求
不符合简单请求的情况下,会触发。需预检的请求 要求必须首先使用
OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。 预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。OPTIONS
是HTTP/1.1
协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。 预检请求中同时携带了下面两个首部字段Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type
1
2首部字段
Access-Control-Request-Method
告知服务器,实际请求将使用POST
方法。首部字段Access-Control-Request-Headers
告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER
与Content-Type
。服务器据此决定,该实际请求是否被允许。HTTP 响应首部字段 (opens new window) ,也就是
CORS
的根本,这些字段是 服务器响应时,需要配置的字段及值,分别如下Access-Control-Allow-Origin
, origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求Access-Control-Allow-Origin: <origin> | *
1Access-Control-Expose-Headers
头让服务器把允许浏览器访问的头放入白名单Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
1Access-Control-Max-Age
头指定了 预检 请求的结果能够被缓存多久,delta-seconds 参数表示 预检 请求的结果在多少秒内有效Access-Control-Max-Age: <delta-seconds>
1Access-Control-Allow-Credentials
头指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容。 也就是在请求是携带 cookie 凭证。在请求携带cookie
时,有一点需要注意,如果服务器端Access-Control-Allow-Origin
设置为通配符*
,请求将会失败。Access-Control-Allow-Credentials: true
1Access-Control-Allow-Methods
首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。Access-Control-Allow-Methods: <method>[, <method>]*
1Access-Control-Allow-Headers
首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。Access-Control-Allow-Headers: <field-name>[, <field-name>]*
1
后端设置响应的
HTTP
响应头是CORS
实现的关键
# postMessage (opens new window)
# postMessage 是什么
postMessage
postMessage 是 html5 引入的 API,postMessage () 方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档,多窗口,跨域消息传递。多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案.
window.postMessage()
方法可以安全地实现跨源通信。
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow
其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。message
将要发送到其他 window 的数据。targetOrigin
通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串*
(表示无限制)或者一个URI
。*如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的 targetOrigin,而不是 。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点transfer
可选。是一串和message
同时传递的 Transferable (opens new window) 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
# 示例
从 a.html
向 iframe
的 b.html
发送消息
<!-- a.html-->
<iframe src="http://localhost:8899/b.html" id="frame" onload="load()"></iframe>
<script>
function load() {
const frame = document.getElementById('frame')
frame.contentWindow.postMessage('from a', 'http://localhost:8000/a.html') // 发送数据
window.onmessage = function (e) { // 接收数据
console.log(e.data) // from b
}
}
</script>
2
3
4
5
6
7
8
9
10
11
<!--b.html-->
<script>
window.onmessage = function (e) { // 接收数据
console.log(e.data) // from a
e.source.postMessage('from b', e.origin)
}
</script>
2
3
4
5
6
7
message
属性具有:
data
从其他window
中传递过来的对象origin
调用postMessage
时消息发送方窗口的origin
source
对发送消息的窗口对象的引用
# window.name + iframe
window.name
window.name 有一个性质, 页面如果设置了 window.name,那么在不关闭页面的情况下,即使进行了页面跳转,这个 window.name 还是会保留
根据 window.name 的这个性质,我们可以实现 跨域访问数据
假设我们需要从 a.html
拿到 c.html
中的数据,那么我们可以找一个和 a.html
同源的页面 b.html
,也就是说
- a 和 b 是同源的
- a 要拿到 c 的数据
实现就是 a 先引用 c,然后 c 设置 window.name ,然后 把 a 的引用地址改为 b
<!--a.html-->
<!--http://localhost:8000/a.html-->
<iframe src="http://localhost:8899/c.html" id="iframe" onload="load"></iframe>
<script>
let type = true
function load() {
const iframe = document.getElementById('iframe')
if (type) {
iframe.src = 'http://localhost:8000/b.html'
type = false
} else {
console.log(iframe.contentWindow.name) // from c
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--c.html-->
<!--http://localhost:8899/c.html-->
<script>
window.name = 'from c'
</script>
2
3
4
5
以 b.html
作为中间页,监听 iframe
第二次的 onload
事件,就可以拿到想要的数据
# location.hash + iframe
这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用 location.hash
来进行传值。
hash
http://www.google.com#abc
中的 #abc
就是 location.hash
, 改变 location.hash
并不会导致页面刷新,所以可以通过 location.hash
来传递数据,当然容量是有限的
# 实现过程
假设,同样有两个页面 a 、 c, a 要和 c 通信
第一步,a 先引用 c
<!--a.html-->
<!--http://localhost:8000/a.html-->
<iframe src="http://localhost:8899/c.html"></iframe>
2
3
在引用时,可以通过 location.hash 传值,也就是
<!--a.html-->
<!--http://localhost:8000/a.html-->
<iframe src="http://localhost:8899/c.html#abc"></iframe>
2
3
第二步,在 c 里面接收传入的 hash,并且传递数据给 a 的同源页面 b
<!--c.html-->
<!--http://localhost:8899/c.html-->
<script>
console.log(location.hash) // abc
const iframe = document.createElement('iframe')
iframe.src = "http://localhost:8000/b.html#c2a"
document.body.appendChild(iframe)
</script>
2
3
4
5
6
7
8
第三步,在 b 页面中,将 hash 传递给 a ,因为 b 和 a 是同源,所以可以传递信息
<!--b.html-->
<!--http://localhost:8000/b.html-->
<script>
window.parent.parent.location.hash = location.hash // b 传递信息给 a, b 的父级是 c ,c 的父级是 a, b 和 a 是同源
</script>
2
3
4
5
第四步,在 a 里面进行监听,然后处理逻辑
<!--a.html-->
<!--http://localhost:8000/a.html-->
<iframe src="http://localhost:8899/c.html#abc"></iframe>
<script>
window.onhashchange = function () {
console.log(location.hash) // c2a
}
</script>
2
3
4
5
6
7
8
这样也就实现了 跨域传递数据
# document.domain + iframe
提示
该方式只能用于主域名相同的情况下,比如 https://a.google.com
和 https://b.google.com
# 原理
原理就是通过设置 document.domain
使得 a 和 b 属于同源,绕过浏览器的检查
注意
端口号是由浏览器另行检查的。任何对 document.domain 的赋值操作,包括 document.domain = document.domain 都会导致端口号被重写为 null 。因此 company.com:8080 不能仅通过设置 document.domain = "company.com" 来与 company.com 通信。必须在他们双方中都进行赋值,以确保端口号都为 null 。
注意
使用 document.domain 来允许子域安全访问其父域时,您需要在父域和子域中设置 document.domain 为相同的值。这是必要的,即使这样做只是将父域设置回其原始值。不这样做可能会导致权限错误。
# 实现
<!--a.html-->
<!--http://a.test.com:8080/a.html-->
<iframe src="http://b.test.com:8080/b.html" onload="load" id="iframe"></iframe>
<script>
document.domain = 'test.com'
function load() {
const iframe = document.getElementById('iframe')
console.log(iframe.contentWindow.temp) // b
}
</script>
2
3
4
5
6
7
8
9
10
<!--b.html-->
<!--http://b.test.com:8080/b.html-->
<script>
document.domain = 'test.com'
var b = 'b'
</script>
2
3
4
5
6
也就是通过设置 document.domain 实现了 跨域通信
# WebSocket (opens new window)
提示
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,WebSocket 本身不存在同源策略限制,所以可以实现跨域。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
# 实现思路
在建立 WebSocket
连接后,你可以通过 send()
方法来向服务器发送数据,并通过 onmessage
事件来接收服务器返回的数据。
<!--客户端-->
<script>
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = function () {
socket.send('客户端') // 向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data) // 接收服务器返回的数据
}
</script>
2
3
4
5
6
7
8
9
10
// 服务端,使用 node.js 模拟
const express = require('express')
const app = express()
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('服务端')
});
})
2
3
4
5
6
7
8
9
10
11
12
13
使用 WebSocket
同样也可以实现跨域
# nginx 反向代理
提示
使用 nginx 反向代理实现跨域,只需要修改 nginx 的配置即可解决跨域问题。
a 网站向 b 网站请求某个接口时,向 b 网站发送一个请求,nginx 根据配置文件接收这个请求,代替 a 网站向 b 网站来请求。 nginx 拿到这个资源后再返回给 a 网站,以此来解决了跨域问题。
# 实现
配置 nginx,监听本地 8080 端口,将 /api 请求转发到 localhost:9000
# nginx.conf
server {
listen 8080;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location /api {
proxy_pass http://localhost:9000;
}
}
2
3
4
5
6
7
8
9
10
11
12
// server.js
let express = require('express')
let app = express()
app.get('/api/getData', function(req, res) {
const list = {'a': 1, 'b': 2}
res.end(JSON.stringify(list))
})
app.listen(9000)
2
3
4
5
6
7
8
在 http://localhost:8080
的 index
页面中,使用 ajax
去请求 /api/getData
, 会得到 JSON
数据
{
"a": 1,
"b": 2
}
2
3
4
这样也就实现了跨域
# nginx 常用命令
nginx -s reload # 优雅重启,并重新载入配置文件nginx.conf
nginx -s quit # 优雅停止nginx,有连接时会等连接请求完成再杀死worker进程
nginx -s reopen # 重新打开日志文件,一般用于切割日志
nginx -v # 查看版本
nginx -t # 检查nginx的配置文件
nginx -h # 查看帮助信息
nginx -c filename # 指定配置文件
2
3
4
5
6
7
8
9
10
11
12
13
# 文档
连前端都看得懂的《Nginx 入门指南》 (opens new window)
# node 中间件实现跨域
提示
跨域问题是浏览器的同源策略的安全机制引起的,服务器之间是不存在跨域问题的,这也不是说服务器之间没有安全机制,只是服务器之间的调用无论是通过 http 访问还是通过 rpc 调用都是协议层面的机制,并没有限制必须同源。 这也就是 Node 层跨域的实质,我们把静态文件和 Node 中间层放在同一个域下,这样前端资源和 Node 层的通信不会受同源策略影响,然后我们通过 Node 中间层把前端的资源请求转发到真实的请求地址,在通过中间层把请求返回的数据传给前端,这样就实现了此域跨彼域的串门操作。
# 实现
本地的 http://localhost:3000/a.html
页面需要请求 http://localhost:9001
的数据,但是 9001
的服务端又不能使用 CORS
的方式来实现跨域,我们可以使用 node
实现一个中间件,达到跨域的效果
第一步,创建一个请求,不请求 9001, 请求 4000
<!--客户端-->
<!--http://localhost:3000/a.html-->
<script>
const xhr = new XMLHttpRequest()
xhr.open('get', 'http://localhost:4000/api/getData')
xhr.send()
xhr.onload = function (){
if (xhr.status === 200) {
console.log(xhr.response)
}
}
xhr.onerror = function() {
throw new Error('error')
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第二步,实现 node 中间件代理转发,以下有两种实现方式
// server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
//设置允许跨域访问该服务.
app.all('*', function (req, res, next) {
console.log(res)
// 设置是否运行客户端设置 withCredentials
// 即在不同域名下发出的请求也可以携带 cookie
// res.header("Access-Control-Allow-Credentials", true)
// 第二个参数表示允许跨域的域名,* 代表所有域名
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS') // 允许的 http 请求的方法
// 允许前台获得的除 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 这几张基本响应头之外的响应头
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With')
if (req.method == 'OPTIONS') {
res.sendStatus(200)
} else {
next()
}
});
const targetUrl = "http://localhost:9001";
app.use('/api/*', createProxyMiddleware({ target: targetUrl, changeOrigin: true }));
//配置服务端口
app.listen(4000, () => {
console.log(`localhost:4000`);
});
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
// server.js
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
// 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
response.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type'
})
// 第二步:将请求转发给服务器
const proxyRequest = http
.request(
{
host: '127.0.0.1',
port: 9001,
path: request.url,
method: request.method,
headers: request.headers
},
serverResponse => {
// 第三步:收到服务器的响应
let body = ''
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
// 第四步:将响应结果转发给浏览器
response.end(body)
})
}
)
.end()
})
server.listen(4000, () => {
console.log('The proxyServer is running at http://localhost:4000')
})
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
第三步, 模拟服务器 9001
端口数据
// 服务端
let express = require('express')
let app = express()
app.get('/api/getData', function(req, res) {
const list = {'a': 1, 'b': 2}
res.end(JSON.stringify(list))
})
app.listen(9001)
2
3
4
5
6
7
8
直接从 3000
请求 9001
的数据是跨域的,不允许的,但是通过 node
中间件之后,数据就可以互通,实现了跨域
# 解释
http://localhost:9001
目标服务器,没有设置跨域,如果在客户端直接请求 9001
端口就会报跨域错误。
http://localhost:4000
node
中间件,因为设置了跨域,所以可以接收 3000
的请求,把 3000
的请求转发到 9001
服务器,9001
服务器返回数据到 4000
,4000
在返回给 3000
,就解决了跨域问题。
http://localhost:3000
客户端
# 最后
CORS
支持所有类型的HTTP
请求,是跨域HTTP
请求的根本解决方案JSONP
只支持GET
请求,JSONP
的优势在于支持老式浏览器,以及可以向不支持CORS
的网站请求数据。- 不管是
Node
中间件代理还是nginx
反向代理,主要是通过同源策略对服务器不加限制。 - 日常工作中,用得比较多的跨域方案是
CORS
和nginx
反向代理