# 10.跨域解决方法
选自:
同源策略和跨域的解决方案 (opens new window) 前端常见跨域解决方案(全) (opens new window) 什么是跨域 & 跨域的 3 种解决方案 (opens new window)
# 哪些需要跨域操作?
调用 XMLHttpRequest
fetchAPI(Ajax)通过跨站点方式访问资源,网络字体,例如 Bootstrap(通过 CSS 使用@font-face 跨域调用字体)。
页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
跨域资源的引入是可以的。但是 js 不能读写加载的内容。如嵌入到页面中的< script src="...">< /script>,< img>,< link>,< iframe>等 (同第二点)
DOM 操作,同源策略禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
也就是 1.) Cookie、LocalStorage 和 IndexDB 无法读取 2.) DOM 和 Js 对象无法获得 3.) AJAX 请求不能发送
# 跨域有风险吗?
跨域请求和 Ajax 技术都会极大地提高页面的体验,但同时也会带来安全的隐患,其中最主要的隐患来自于CSRF跨站请求伪造
。
- 用户通过浏览器,访问正常网站 A(例如某银行),通过用户的身份认证(比如用户名/密码)成功 A 网站;
- 网站 A 产生 Cookie 信息并返回给用户的浏览器;
- 用户保持 A 网站页面登录状态,在同一浏览器中,打开一个新的 TAB 页访问恶意网站 B;
- 网站 B 接收到用户请求后,返回一些攻击性代码,请求 A 网站的资源(例如转账请求);
- 浏览器执行恶意代码,在用户不知情的情况下携带 Cookie 信息,向网站 A 发出请求。
- 网站 A 根据用户的 Cookie 信息核实用户身份(此时用户在 A 网站是已登录状态),A 网站会处理该请求,导致来自网站 B 的恶意请求被执行。
# 跨域的解决方案
- 简单的跨域请求 jsonp 即可
- 复杂的 cors
- 窗口之间 JS 跨域 postMessage
- 开发环境下接口跨域用 nginx 反向代理或 node 中间件比较方便。
# 1. 降域 document.domain
特点:
- 只能在父域名与子域名之间使用,且将 xxx.child1.a.com 域名设置为 a.com 后,不能再设置成 child1.a.com
- 存在安全性问题,当一个站点被攻击后,另一个站点会引起安全漏洞
- 这种方法只适用于 Cookie 和 iframe 窗口
# 2. JSONP
JSONP 缺点:只能实现 get 一种请求。
JSONP 实现跨域请求的原理:简单的说,就是动态创建< script>
标签,然后利用< script>
的 src 属性不受同源策略约束来跨域获取数据。
JSONP 由两部分组成:回调函数 和 数据。回调函数是用来处理服务器端返回的数据,回调函数的名字一般是在请求中指定的。而数据就是我们需要获取的数据,也就是服务器端的数据。
不受同源策略限制的:
<script src="...">`//加载图片到本地执行
<img src="..."> //图片
<link href="...">//css
<iframe src="...">//任意资源
2
3
4
5
6
7
8
9
10
# JSONP 的简单实现过程:
const jsonp = function (url, data) {
return new Promise((resolve, reject) => {
// 初始化url
let dataString = url.indexOf("?") === -1 ? "?" : "&";
let callbackName = `jsonpCB_${Date.now()}`;
console.log(callbackName);
url += `${dataString}callback=${callbackName}`;
if (data) {
// 有请求参数,依次添加到url
for (let k in data) {
url += `&${k}=${data[k]}`;
}
}
let jsNode = document.createElement("script");
jsNode.src = url;
// 触发callback,触发后删除js标签和绑定在window上的callback
window[callbackName] = (result) => {
delete window[callbackName];
document.body.removeChild(jsNode);
if (result) {
resolve(result);
} else {
reject("没有返回数据");
}
};
// js加载异常的情况
jsNode.addEventListener(
"error",
() => {
delete window[callbackName];
document.body.removeChild(jsNode);
reject("JavaScript资源加载失败");
},
false
);
// 添加js节点到document上时,开始请求
document.body.appendChild(jsNode);
});
};
jsonp("https://suggest.taobao.com/sug", {
code: "utf-8",
q: "%E5%8D%AB%E8%A1%A3",
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
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
192.168.10.14/1.txt 的代码,设置回调函数,数据以 JSON 格式存放
handleResponse([ { "name":"xie",
"sex" :"man",
"id" : "66" },
{ "name":"xiao",
"sex" :"woman",
"id" : "88" },
{ "name":"hong",
"sex" :"woman",
"id" : "77" }]
)
2
3
4
5
6
7
8
9
10
然后当我们点击了 确定按钮后,console 控制台就输出了从 192.168.10.14/1.js 传过来的 JSON 格式的数据了
# jsonp 跨域存在的问题:
- 使用这种方法,只要是个网站都可以拿到 b.com 里的数据,存在安全性问题。目前已知的有Referer 校验和 Token 校验。
- 只能是 GET,不能 POST
- 可能被注入恶意代码,篡改页面内容,可以采用字符串过滤来规避此问题
- 需要服务器改动代码
# 3、CORS 跨域资源共享(重点掌握)
- 实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信
# 客户端需要做什么?
各主流的浏览器都会对动态的跨域请求进行特殊的验证处理。验证处理分为简单请求验证处理和预先请求验证处理。
# 两种请求
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
请求方法是下列之一:
GET
HEAD
POST
请求头中的 Content-Type 请求头的值是下列之一:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
- application/x-www-form-urlencoded: 参数的格式为 key=value&key=value,即普通 GET 那种类型的传递方式
- multipart/form-data:表单提交文件必须要用这种格式,不会进行编码,而是以分割线的形式传递,分割线的值是----WebxxxxxxxxxxAJv3。
- text/plain:text/plain 是以纯文本格式(就是一段字符串)发送的. 如果你发送一个对象例如{ name:"leiwuyi", age:12 }一定要对它做 JSON.stringfiy()处理,否则将传送[object Object]
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
# 简单请求
简单请求时,浏览器会直接发送跨域请求,并在请求头中携带 Origin 的 header,表明这是一个跨域的请求。
服务器端接到请求后,会根据自己的跨域规则,通过
Access-Control-Allow-Origin
和Access-Control-Allow-Methods
响应头,来返回验证结果。 如果验证成功,则会直接返回访问的资源内容。
# 复杂请求
一般是发送 JSON 格式的 ajax 请求,或带有自定义头的请求
对于非简单请求的跨源请求,浏览器会在真实请求发出前,增加一次 OPTION 请求,称为预检请求(preflightrequest)。预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到 HTTP 头信息字段中,询问服务器是否允许这样的操作
例如一个 GET 请求的预检请求,包含一个自定义参数 X-Custom-Header
OPTIONS /test HTTP/1.1 Origin: http://www.test.com
Access-Control-Request-Method: GET // 请求使用的 HTTP 方法
Access-Control-Request-Headers: X-Custom-Header // 请求中包含的自定义头字段
Host: www.test.com
2
3
4
服务器收到请求时,需要分别对 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 进行验证,验证通过后,会在返回 HTTP 头信息中添加:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://www.test.com // 允许的域
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的方法
Access-Control-Allow-Headers: X-Custom-Header // 允许的自定义字段
Access-Control-Allow-Credentials: true // 是否允许用户发送、处理 cookie
Access-Control-Max-Age: 172800 //
预检请求的有效期,单位为秒。有效期内,不需要发送预检请求,ps 48小时
2
3
4
5
6
当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。
所以后端处理其实处理的就是这次预检请求
两种请求就介绍到这里,其他详见:同源策略和跨域的解决方案 (opens new window)
# CORS 怎么设置
普通跨域请求:只服务端
设置Access-Control-Allow-Origin
即可,前端无须设置,若要带 cookie 请求:前后端都需要设置(withCredentials 属性 )。
需注意的是:由于同源策略的限制,所读取的 cookie 为跨域请求接口所在域的 cookie,而非当前页。如果想实现当前页 cookie 的写入,可参考下文:七、nginx 反向代理中设置 proxy_cookie_domain 和 八、NodeJs 中间件代理中 cookieDomainRewrite 参数的设置。
- 前端设置:
1.)原生 ajax
// 前端设置是否带cookie
xhr.withCredentials = true;
2
示例代码:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open("post", "http://www.domain2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
- 服务端设置:
若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
1.)Nodejs 后台示例:
server.on("request", function (req, res) {
req.addListener("end", function () {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
"Access-Control-Allow-Credentials": "true", // 后端允许发送Cookie
"Access-Control-Allow-Origin": "http://www.domain1.com", // 允许访问的域(协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
"Set-Cookie": "l=a123456;Path=/;Domain=www.domain2.com;HttpOnly",
// HttpOnly的作用是让js无法读取cookie
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4、node 正向代理
/api -> 同域的 node 服务 ->/api -> 前端
node 中间件实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中域名,实现当前域的 cookie 写入,方便接口登录认证。
# 5、nginx 反向代理 proxy_pass
代理到定义的地址上
/ api -> /same/api 实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。
# 6、WebSocket 协议跨域
WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。
# 7、postMessage 跨域
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题: a.) 页面和其打开的新窗口的数据传递 b.) 多窗口之间消息传递 c.) 页面与嵌套的 iframe 消息传递