什么是跨域?
1.首先你得先了解同源策略
同源策略是浏览器最核心也是最基本的安全功能,它是指"协议+域名+端口"三者相同的一种约定,即便两个不同的域名指向同一个ip地址,也非同源。如果缺少了同源策略,浏览器就更容易受到XSS、CSRF等攻击。
同源策略限制的内容有
- LocalStorage、IndexedDB 等存储性内容,以源进行分割。每个源都拥有自己单独的存储空间,一个源中的Javascript脚本不能对属于其它源的数据进行读写操作。Cookie则相对更特殊,一个页面可以为本域和任何父域设置cookie,只要是父域不是公共后缀(public suffix)即可。所以同源的网页读取 cookie 时,不会知晓它的出处。
- DOM无法获得
- AJAX 请求发送失败(可以发送出去,但是被浏览器拦截了)
但是有可以跨域加载资源的例外
<script src="..."></script>
但是语法错误信息只能在同源脚本中捕捉到。<link rel="stylesheet" href="...">
标签嵌入CSS。<img>
嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,...<video> 和 <audio>
嵌入多媒体资源。@font-face
引入的字体。<frame> 和 <iframe>
载入的任何资源。<object>, <embed> 和 <applet>
的插件。
2.常见的跨域场景
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:
特别说明两点:
- 如果是协议和端口造成的跨域问题“前台”是无能为力的。
- 在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”翻译成人话就是“协议, 域名和端口必须匹配”。而不会因为你两个域名都同时映射到相同IP就把你当成同源了,你还是不同源!
请求跨域了,那么请求到底发出去没有?
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
CSRF和XSS到底是个啥东东?
- CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。
例如:前端构造一个恶意页面,收集用户信任网站的服务端的敏感信息,再请求用户信任网站接口。如此一来服务端就会把恶意页面当成用户了,接下来就...
解决方法:验证接口的调用来源(Referer),服务端判断Referer是否是白名单,或者部署随机Token来防御。 - XSS漏洞:页面被攻击者注入了恶意的代码,泄露数据
例如:不严谨的content-type
导致的 XSS 漏洞,想象一下 JSONP 就是你请求http://youdomain.com?callback=douniwan
, 然后返回douniwan({ data })
,那假如请求http://youdomain.com?callback=<script>alert(1)</script>
不就返回<script>alert(1)</script>({ data })
了吗,如果没有严格定义好Content-Type( Content-Type: application/json )
,再加上没有过滤 callback 参数,直接当 html 解析了,就是一个赤裸裸的 XSS 了。
解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback 后的参数并且限制长度(进行字符转义,例如<换成<,>换成>)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。并且对于链接跳转,如<a href="xxx" 或 location.href="xxx"
,要检验其内容,禁止以javascript:
开头的链接,和其他非法的scheme
。
跨域解决方案
1.CORS
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。
该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求
只要同时满足以下两大条件,就属于简单请求 条件1:使用下列方法之一:
- GET
- HEAD
- POST
条件2:Content-Type 的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
或者请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
复杂请求
不符合以上条件的请求就肯定是复杂请求了。因为我们的Content-Type绝大多数是application/json,所以我们日常中大部分都是复杂请求。
复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求。
浏览器会先发起OPTIONS方法到服务器,以获知服务器是否允许该实际请求。
讲了这么多,上个例子呗
// index.html
// 使用日常工作中用到的axios封装的请求直接干 - -
initPost() {
// 普通请求 需设置跨域
say({ callback: "dudu" }).then(data => (console.log(data)));
}
// cors.js (node端)
let express = require("express");
let app = express();
let whitList = ["http://localhost:8088"]; //设置白名单 因为我上面项目启动端口是8088
app.use(function(req, res, next) {
let origin = req.headers.origin;
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader("Access-Control-Allow-Origin", origin);
// 允许携带哪个头访问我
res.setHeader("Access-Control-Allow-Headers", "sessionno");
// 允许哪个方法访问我 可以设置*
res.setHeader("Access-Control-Allow-Methods", "GET");
// 允许携带cookie
res.setHeader("Access-Control-Allow-Credentials", true);
// 预检的存活时间
res.setHeader("Access-Control-Max-Age", 6);
// 允许返回的头
res.setHeader("Access-Control-Expose-Headers", "sessionno");
if (req.method === "OPTIONS") {
res.end(); // OPTIONS请求不做任何处理
} else {
next();
}
}
});
app.get("/say", (req, res) => {
let { wd, callback } = req.query;
console.log(wd);
console.log(callback);
res.setHeader("sessionno", "sakura"); //返回一个响应头,后台需设置
res.end(`${callback}('hello world')`);
});
app.use(express.static(__dirname));
app.listen(3000);
上述代码由http://localhost:8088/index.html
向http://localhost:3000/
跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键。
2.nginx 反向代理
实现原理:搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口。
先下载nginx, 记得不同系统需下载不同的文件哦
然后将nginx目录下的conf的nginx.conf修改如下:
# proxy服务器
server {
listen 81;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://10.1.21.20:4444/; #反向代理
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://localhost:8088; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header Access-Control-Allow-Credentials true;
}
}
最后通过命令行nginx -s reload
重启nginx (或者start nginx
)
// index.html
// nginx
initNginx() {
let xhr = new XMLHttpRequest();
xhr.withCredentials = true; // 浏览器是否读写cookie
xhr.open("get", "http://localhost:81/?user=admin", true);
xhr.send();
let that = this;
xhr.onload = function(data) {
// 请求结束后,在此处写处理代码
console.log(data);
console.log(data.currentTarget.response);
that.nginx = data.currentTarget.response;
};
}
// nginx.js (node端)
let http = require("http");
let server = http.createServer();
let qs = require("querystring");
let count = 0;
server.on("request", (request, response) => {
var params = qs.parse(request.url.substring(2));
console.log(params);
response.write(JSON.stringify(params) + count++);
response.end();
});
server.listen("4444");
console.log("Server is running at port 4444...");
3.Node中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
原理类似于nginx反向代理
代理服务器,需要做以下几个步骤:
- 接受客户端请求 。
- 将请求转发给服务器。
- 拿到服务器 响应 数据。
- 将 响应 转发给客户端。
接下来我们用日常工作中用到的接口通过代理服务器向我们的后台服务器进军!
// index.html
// node中间件转发
_msgSend() {
const params = {
mobile: "13333333333",
tmCode: "3001"
};
msgSend(params).then(res => {
console.log(res);
});
},
// middleware.js (node端)
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, sessionno"
});
console.log(request.url);
// 第二步 将请求转发给服务器
const proxyRequest = http
.request(
{
host: "xxx.xxx.xxx.xxx",
port: 8082,
url: request.url,
path: request.url, // 必须要设置path
method: request.method,
headers: request.headers
},
serverResponse => {
// 第三步 收到服务器的响应
var body = "";
serverResponse.on("data", chunk => {
console.log("收到");
body += chunk;
});
serverResponse.on("end", () => {
// 第四步 将响应结果转发给浏览器
console.log(body);
response.end(body);
});
}
)
.end();
});
server.listen(3000, () => {
console.log("The proxyServer is running at http://localhost:3000");
});
上述代码经过两次跨域,一次是浏览器向代理服务器发送请求,第二次是代理服务器将响应结果转发给浏览器
4.JSONP
JSONP原理
利用 <script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
JSONP的实现流程
- 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
- 创建一个
<script>
标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show
)。 - 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是
show('我不爱你')
。 - 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数
(show)
,对返回的数据进行操作。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。
// index.html
// JSONP
jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
window[callback] = function(data) {
resolve(data);
document.body.removeChild(script);
};
params = { ...params, callback }; // wd=b&callback=show
let arrs = [];
for (let key in params) {
arrs.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arrs.join("&")}`;
document.body.appendChild(script);
});
},
// init
initJsonp() {
this.jsonp({
url: "http://localhost:3000/say",
params: { wd: "Iloveyou" },
callback: "show"
}).then(data => {
console.log(data);
});
},
// jsonp.js (node 端)
let express = require("express");
let app = express();
app.get("/say", (req, res) => {
let { wd, callback } = req.query;
console.log(wd);
console.log(callback);
res.end(`${callback}('hello world')`);
});
app.listen(3000);
上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show
这个地址请求数据,然后后台返回show('hello world')
,最后会运行show()
这个函数,打印出'hello world'
jQuery的jsonp形式
JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存
$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});