那些年那些跨域的事
1. 什么是跨域?
出于浏览器的同源策略限制,浏览器会拒绝跨域请求。那什么样的请求才算是跨域请求呢?
- 协议不相同
- 域名不相同
- 端口不相同
2. 跨域请求举例
假设我们现在浏览器中的地址是http://www.zaoren.com
URL | 是否跨域 | 原因 |
---|---|---|
http://www.zaoren.com/index | 否 | 同源 |
https://www.zaoren.com | 是 | 协议不同 |
http://www.zaorenll.com | 是 | 域名不同 |
http://www.zaorenll.com:8080 | 是 | 端口号不同 |
3. 为什么会有跨域请求呢?
- 场景一: 项目工程化后,一个需求可能对应到多个服务。而往往不同服务是放在不同的服务器上的,尽管在同一台服务器上部署,那么端口号也是不一样同的。
- 场景二: 当我们使用FileReader()去读取本地文件(比如C://Documents…)的时候,往往会跨域,因为此时的协议是不相同的(比如我引用了第三方的库objLoader.js去加载一个obj文件,就会报跨域的错,这时候需要本地启一个服务,或者把资源放到能被所有用户访问到的服务器上)。
可能有朋友就会问了:不可能!我写程序都已经写了好几个月了!读取了成百上千的js和img文件,为什么从来没有遇到过跨域的问题?而且主要是面试官还老问我跨域的问题,要不然我才不会点开这篇博客来看呐。
偷偷告诉你,朋友,你之前可能一直使用的是专门为跨域请求提供的一种解决方案:JSONP
4.如何解决跨域问题?
在这里先说一个笑话,之前在学校的时候学习Three.js这个库,由于Three.js需要通过loader去加载本地的一些文件,而加载文件的时候会跨域,所以老师倡导我们每个人都装一个Hbuilder,然后用Hbuilder打开官方提供的demo,就能正常运行了。
其实我们在使用Hbuilder启动项目的时候,编译器会帮我们自动启动一个服务,然后从项目的根目录上开始,将整个文件放在这个服务下,那么这个时候情况就和表格中的第一种情况一样,localhost://127.0.0.1:83992访问localhost://127.0.0.1:83992/src/obj/objDemo.obj。
那么我们实际开发中有那些跨域的解决方案呢?下面仅介绍以下三种方法
- JSONP(上面提到过)
- CORS 跨域资源共享(重点!)
- Nginx反向代理
4.1 JSONP的跨域方式
JSONP是单纯的为了实现跨域请求而实现的一个方式。虽然因为同源策略的影响,不能通过XMLHttpRequest请求不同域上的数据。但在页面上通过html标签去引入不同域上的js脚本文件却是可以的。通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在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>
</head>
<body>
<script>
function show(json) {
console.log(json.s);
}
</script>
<script src="https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=web&cb=show"></script>
</body>
</html>
4.2 CORS的跨域方式
跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种机制,该机制使用附加的 HTTP头来告诉浏览器,准许运行在一个源上的Web应用访问位于另一不同源选定的资源。
4.2.1 预请求
此规则规定,处于跨域环境并在下面几个条件下,浏览器会发送两个请求给服务器;
举例说明:假设客户端需要发起PUT请求
- 首先,客户端需要发送OPTIONS求情给服务器
- 在服务器内部,需要对OPTIONS请求,做一些设定,告诉客户端是否允许访问
- 客户端确认服务器允许该方法,最终发送PUT请求;否则,跑出错误,服务器拒绝访问此方法。
触发预发请求有三种情况:
- 使用了某些方法,比如说 PUT, DELETE 等。
- Fetch 规范规定了对 CORS安全的首部字段集合,人为设置会触发预请求。
- Content-Type的值不是text/plain ,multipart/form-data,application/x-www-form-urlencoded
判断是否需要发送预请求的详细链接HTTP访问控制(CORS)
4.2.2 非预请求
此规则规定,在跨域产生的条件下,某些请求和方法,不需要预请求,只发送一个请求就行了。比如GET,POST和HEAD这三个方法。
接下来,我们来模拟一次不需要发预请求的简单请求(以下称简单请求)
前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Ajax测试</title>
</head>
<body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
$.ajax({
url: "http://localhost:3000",
type: "get",
success: function (result) {
console.log(result);
},
error: function (msg) {
console.log(msg);
},
});
</script>
</body>
</html>
后端代码
const http = require("http");
const server = http.createServer((request, response) => {
if (request.url === "/") {
if (request.method === "GET") {
console.log('recevied');
response.end("{name: 'BruceLee', password: '123456'}");
}
if (request.method === "POST") {
console.log('recevied');
response.end("true");
}
if(request.method === 'OPTIONS') {
console.log('发起了一个预请求');
response.end(JSON.stringify({state:true}));
}
}
response.end("false");
});
server.listen(3000, () => {
console.log("The server is running at http://localhost:3000");
});
查看结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9DnHRty-1600861428589)(https://imgkr2.cn-bj.ufileos.com/3a25f34e-d680-49b6-9240-57c2489aea0f.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=Ko4cJjGMCD0GXvq7vwVg%252FasBFtg%253D&Expires=1600943129)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fg7UkorD-1600861428591)(https://imgkr2.cn-bj.ufileos.com/688eb10a-a72d-4b69-a49f-e6a03f6bd0dd.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=n5bft2f%252FwixpmQ1ufeG4%252F%252FZ2sIQ%253D&Expires=1600943142)]
很明显,我们后端已经收到请求了,但是返回的数据被浏览器的同源策略劫持了。
4.2.3 处理没有预请求的跨域请求
关键代码:设置一条 响应首部字段,允许 CORS 跨域资源共享:
if (request.method === "GET") {
console.log('recevied');
response.writeHead(200, {
'Access-Control-Allow-Origin': '*'
});
response.end("{name: 'BruceLee', password: '123456'}");
}
跨域请求成功了!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-59D5dIMP-1600861428592)(https://imgkr2.cn-bj.ufileos.com/6b6c5d76-92f6-42dd-a6b4-78b68c253d05.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=UsHeggdQimUG4E9DncBqBa9xydY%253D&Expires=1600943578)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yAeHZiTA-1600861428593)(https://imgkr2.cn-bj.ufileos.com/ea0dfd6a-fb8a-475c-935c-0bc43c8406ff.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=XKAX5hNOnobJPt62wbXFuQ%252Fepb8%253D&Expires=1600943417)]
处理 非预请求 就是这么简单,只需要在后台设置,一条 响应首部字段 即可。
4.2.4 处理有预请求的跨域请求
POST 方法在设置 contentType 为 application/json 时会触发预请求。
前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Ajax测试</title>
</head>
<body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var data = { name: "Zaoren", password: "123456" };
$.ajax({
url: "http://localhost:3000",
type: "post",
data: JSON.stringify(data),
contentType: "application/json;charset=utf-8",
success: function (result) {
console.log(result);
},
error: function (msg) {
console.log(msg);
},
});
</script>
</body>
</html>
后端代码
const http = require("http");
const server = http.createServer((request, response) => {
if (request.url === "/") {
if (request.method === "GET") {
console.log('recevied');
response.writeHead(200, {
'Access-Control-Allow-Origin': '*'
});
response.end("{name: 'Zaoren', password: '123456'}");
}
if (request.method === "POST") {
console.log('recevied');
response.end("true");
}
if(request.method === 'OPTIONS') {
console.log('发起了一个预请求');
response.end(JSON.stringify({state:true}));
}
}
response.end("false");
});
server.listen(3000, () => {
console.log("The server is running at http://localhost:3000");
});
查看前后端效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SL1OZBxZ-1600861428595)(https://imgkr2.cn-bj.ufileos.com/fb32ef04-dbef-431b-bd49-779978c6aa20.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=q9fdZHMuH39pOtNEJ%252BnVyLLPxlM%253D&Expires=1600944303)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1lL02Wlw-1600861428597)(https://imgkr2.cn-bj.ufileos.com/c101fbb4-5e33-401d-a375-75fbf1b61db5.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=I7GHOg8oG6wzZ4hPodyvxzN8YCU%253D&Expires=1600944284)]
看到,如我们所愿,我们发了一个预请求,但是我们的请求还是跨域了!很明显,是因为我们OPTIONS方法内部没有设置CORS响应首部字段,所以出现跨域错误。
修改代码:
if (request.method === 'OPTIONS') {
response.writeHead(200, {
'Access-Control-Allow-Origin': '*', // 设置 optins 方法允许所有服务器访问
'Access-Control-Allow-Methods': '*', // 允许访问 POST PUT DELETE 等所有方法
});
response.end( JSON.stringify({state: true}) );
}
查看结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zf6ycYUM-1600861428597)(https://imgkr2.cn-bj.ufileos.com/67c07ebe-54cf-4b20-bd64-23bf00631ad5.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=Th%252FdWWk3bfwsXYLogPbHEGgSSKc%253D&Expires=1600944746)]
嗯? 按照报错信息,我们应该设置Access-Control-Allow-Headers响应首部字段。
最终修改
const http = require("http");
const server = http.createServer((request, response) => {
if (request.url === "/") {
if (request.method === "GET") {
console.log("recevied");
response.writeHead(200, {
"Access-Control-Allow-Origin": "*",
});
response.end("{name: 'Zaoren', password: '123456'}");
}
if (request.method === "POST") {
console.log("recevied");
response.writeHead(200, {
"Access-Control-Allow-Origin": "*",
});
response.end("true");
}
if (request.method === "OPTIONS") {
console.log('发起了一个预请求!');
response.writeHead(200, {
"Access-Control-Allow-Origin": "*", // 设置 optins 方法允许所有服务器访问
"Access-Control-Allow-Methods": "*", // 允许访问 POST PUT DELETE 等所有方法
"Access-Control-Allow-Headers": "*"
});
response.end(JSON.stringify({ state: true }));
}
}
response.end("false");
});
server.listen(3000, () => {
console.log("The server is running at http://localhost:3000");
});
查看结果:没错,是成功的两个请求了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2SlrnU7Q-1600861428598)(https://imgkr2.cn-bj.ufileos.com/e0ab904a-6781-4272-b176-2019d473d0d5.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=7NeOV4WC5sFFrXSvej%252BjtlOmwqE%253D&Expires=1600944952)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q4WmhWMQ-1600861428598)(https://imgkr2.cn-bj.ufileos.com/debe3048-c9a4-43e0-bafb-b9c5d8247ed5.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=bWU1sVeKP7PlD2d6FTy3eBtiSZk%253D&Expires=1600944988)]
总结:
-
使用 CORS 跨域资源共享,是需要分成 预请求 与 非预请求 处理的。
-
非预请求,在服务器内,只需要简单设置:
‘Access-Control-Allow-Origin’: '*
复制代码 -
预请求,在服务器内,至少要设置三个 响应首部字段:
'Access-Control-Allow-Origin': ?,
'Access-Control-Allow-Methods': ?,
'Access-Control-Allow-Headers': 'Content-Type',
- 前端使用 content-Type: application/json 的时候,必须注意这是 预请求 ,后端需要处理 OPTIONS 方法
4.3 使用Nginx代理
之前说了,跨域是浏览器的行为,那么如果我用一台服务器专门转发我的请求。跨域的问题就迎刃而解了。目前用的比较多的是nignx做代理,解决跨域问题。