# 10.跨域解决方法

选自:

同源策略和跨域的解决方案 (opens new window) 前端常见跨域解决方案(全) (opens new window) 什么是跨域 & 跨域的 3 种解决方案 (opens new window)

# 哪些需要跨域操作?

  1. 调用 XMLHttpRequest

  2. fetchAPI(Ajax)通过跨站点方式访问资源,网络字体,例如 Bootstrap(通过 CSS 使用@font-face 跨域调用字体)。

  3. 页面中的链接,重定向以及表单提交是不会受到同源策略限制的。

  4. 跨域资源的引入是可以的。但是 js 不能读写加载的内容。如嵌入到页面中的< script src="...">< /script>,< img>,< link>,< iframe>等 (同第二点)

  5. DOM 操作,同源策略禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。

    也就是 1.) Cookie、LocalStorage 和 IndexDB 无法读取 2.) DOM 和 Js 对象无法获得 3.) AJAX 请求不能发送

# 跨域有风险吗?

跨域请求和 Ajax 技术都会极大地提高页面的体验,但同时也会带来安全的隐患,其中最主要的隐患来自于CSRF跨站请求伪造在这里插入图片描述

  1. 用户通过浏览器,访问正常网站 A(例如某银行),通过用户的身份认证(比如用户名/密码)成功 A 网站;
  2. 网站 A 产生 Cookie 信息并返回给用户的浏览器;
  3. 用户保持 A 网站页面登录状态,在同一浏览器中,打开一个新的 TAB 页访问恶意网站 B
  4. 网站 B 接收到用户请求后,返回一些攻击性代码,请求 A 网站的资源(例如转账请求);
  5. 浏览器执行恶意代码,在用户不知情的情况下携带 Cookie 信息,向网站 A 发出请求
  6. 网站 A 根据用户的 Cookie 信息核实用户身份(此时用户在 A 网站是已登录状态),A 网站会处理该请求,导致来自网站 B 的恶意请求被执行。

# 跨域的解决方案

  1. 简单的跨域请求 jsonp 即可
  2. 复杂的 cors
  3. 窗口之间 JS 跨域 postMessage
  4. 开发环境下接口跨域用 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="...">//任意资源


1
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);
	});
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

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" }]
)
1
2
3
4
5
6
7
8
9
10

然后当我们点击了 确定按钮后,console 控制台就输出了从 192.168.10.14/1.js 传过来的 JSON 格式的数据了 在这里插入图片描述

# jsonp 跨域存在的问题:

  1. 使用这种方法,只要是个网站都可以拿到 b.com 里的数据,存在安全性问题。目前已知的有Referer 校验和 Token 校验
  2. 只能是 GET,不能 POST
  3. 可能被注入恶意代码,篡改页面内容,可以采用字符串过滤来规避此问题
  4. 需要服务器改动代码

# 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]

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

# 简单请求
  1. 简单请求时,浏览器会直接发送跨域请求,并在请求头中携带 Origin 的 header,表明这是一个跨域的请求。

  2. 服务器端接到请求后,会根据自己的跨域规则,通过Access-Control-Allow-OriginAccess-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
1
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小时
1
2
3
4
5
6

当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。

所以后端处理其实处理的就是这次预检请求

两种请求就介绍到这里,其他详见:同源策略和跨域的解决方案 (opens new window)

# CORS 怎么设置

下面选自:前端常见跨域解决方案(全) (opens new window)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带 cookie 请求:前后端都需要设置(withCredentials 属性 )

需注意的是:由于同源策略的限制,所读取的 cookie 为跨域请求接口所在域的 cookie,而非当前页。如果想实现当前页 cookie 的写入,可参考下文:七、nginx 反向代理中设置 proxy_cookie_domain 和 八、NodeJs 中间件代理中 cookieDomainRewrite 参数的设置。

  1. 前端设置:

1.)原生 ajax

// 前端设置是否带cookie
xhr.withCredentials = true;
1
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);
	}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

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
		});
	});
});
1
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 消息传递

Last Updated: 6/3/2024, 1:08:34 AM