跨域,指的是从一个域名去请求另外一个域名的资源,即跨域名请求。跨域时,浏览器不能执行其他域名网站的脚本,这是由浏览器的同源策略造成的,是浏览器施加的安全限制, 跨域限制访问,其实是浏览器的限制。 同源策略是浏览器最核心也最基本的安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源 ,这是一个用于隔离潜在恶意文件的重要安全机制。所以跨域问题只在浏览器中出现,如果客户端是APP的话,那跨域问题就不存在了。 PS:IE端口除外,IE对同源策略的定义有略微的不同,具体可以查看文末给出的同源策略的链接。
所谓同源是指:域名,协议,端口相同,即两个资源具有相同的源。 只要三者之间有一个不同,就是跨域(跨源)。
URL | 说明 | 同源检测结果 | 是否跨域 |
---|---|---|---|
http://www.abc.com/a.html http://www.abc.com/b.html |
域名 端口 协议相同 | 成功 | 否 |
http://www.abc.com/dir1/a.html http://www.abc.com/dir2/b.html |
域名 端口 协议相同,路径不同 | 成功 | 否 |
http://www.abc.com:8080/a.html http://www.abc.com/b.html |
域名 协议相同,端口不同 | 失败 | 是 |
http://www.abc.com/a.html https://www.abc.com:80/b.html |
域名 端口相同,协议不同 | 失败 | 是 |
http://www.abc.com/a.html http://59.68.92.100/b.html |
域名和域名对应的IP | 失败 | 是 |
http://www.abc.com/a.html http://blog.abc.com/b.html |
二级域名相同,三级域名不同 | 失败 | 是 |
http://www.abc.com/a.html http://abc.com/b.html |
二级域名相同,三级域名不同 | 失败 | 是 |
http://www.abc.com/a.html http://www.aaa.com/b.html |
协议 端口相同,域名不同 | 失败 | 是 |
Cookie
、LocalStorage
和 IndexedDB
DOM
和JS
对象进行操作AJAX
请求注:本文只专注于AJAX请求跨域,其他跨域类型不做过多深入。
实现跨域的方式有很多种,比如JSONP、CORS、http-proxy、nginx、websocket
、跨站脚本API访问,如:postMessage、document.domain
等。
由于同源策略的限制,AJAX
请求是不允许进行跨域请求的,但是在HTML中
,拥有src
和href
属性的标签是可以跨域请求外部资源的,如link、script、img
等(值得注意的是,不同标签允许的交互类型貌似是不同的,分别为跨域写、跨域资源嵌入、跨域读,暂时不知道这些标签可以发送跨域请求的原因,貌似是历史遗留问题,有知道的大佬可以指点一下),根据标签的特性,开发人员想到了一个解决跨域请求的方法,即
JSONP
,全名 JSON with padding
。
为了方便进行实验(其实是为了偷懒),我找了一个百度的JSONP
接口https://www.baidu.com/sugrec?prod=pc&wd=什么是JSONP&cb=getData
,该接口的特点是:你输入一个指定的函数名,然后服务器会根据函数名返回一串JS
的函数调用格式的字符串,比如我们指定的函数名为getData
,然后百度的服务器会根据我们指定的函数名返回如下内容:
getData({"q":"什么是jsonp","p":true,"g":[{"type":"sug","sa":"s_1","q":"什么是jsonp异步处理方法"}],"slid":"16695766345172117423"})
让我们仔细观察一下服务器返回的内容,你是否发现这种格式很熟悉?这不就是 函数名 + (+ 参数 +)
的格式吗?如果我们有一个名为getData
、形参是一个对象的JS
函数,是不是就意味着我们可以把服务器返回的数据看成是一段调用了一个函数名为getData
、形参是一个对象的函数的JS
代码呢?答案是显而易见的,肯定是可以的嘛。那怎么让服务器返回的数据变成一段JS
代码呢?我们都知道,我们可以在HTML页面里面编写JS
代码——只需将JS
代码用标签括起来就可以让代码在页面加载的时候就运行了。前面我们也提到了
标签的
src
属性是可以跨域请求外部资源的,如果我们将我们要访问的接口做为src
属性的值,那我们不就可以访问该跨域接口了吗?如果我们再提供一个全局的函数getData
,用来对接口返回的数据进行操作,那岂不是就实现了跨域请求?是的,确实可以,而这就是JSONP
的实现原理。这里的getData
函数就是一个callback函数。让我们整理一下JSONP
的原理:
在了解完JSONP
的原理之后,我们来看看具体的JS
代码实现:
function jsonp({url, params, cb}) {
return new Promise((resolve, reject) => {
//
let scriptDom = document.createElement('script');
//定义一个全局函数,函数名为cb的值,此函数会由服务器的内容来调用
window[cb] = function (data) {
resolve(data);
document.body.removeChild(scriptDom);
};
params = {...params, cb}
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`);
}
scriptDom.src = `${url}?${arrs.join('&')}`;
//为了让代码执行需要script对象添加Dom树中
document.body.appendChild(scriptDom);
})
};
jsonp({
url: 'https://www.baidu.com/sugrec',
params: {prod: 'pc', wd: '什么是JSONP'},
cb: 'getData'
}).then(data => {
console.log(data);
});
代码所做的事情就是我们在前面列出来的三点,当然为了方便我是用了Promise来写,不了解这个的可以去网上搜一下,或者是查一下别的JSONP
代码的实现,当然代码不是最重要的,理解了原理,代码写起来也就不难了。
为了方便大家理解服务器所干的事情,我也写了一段PHP代码供大家参考,如果大家需要其他语言版本的,去百度或者Google找一下就行了。
简单地说就是将前端传过来的方法名和关键词拼接成一串JS
函数调用格式的字符串返回。
缺点:只支持GET请求,不支持其他请求,有XSS攻击的风险。
同源策略默认阻止“跨域”获取资源。但是跨域资源共享CORS
给了web服务器这样的权限:服务器可以选择是否允许跨域请求访问到它们的资源。
跨域资源共享(CORS
)是一种机制, 它由一系列的HTTP
头组成,这些HTTP
头决定浏览器是否阻止前端 JavaScript
代码获取跨域请求的响应,从而克服了AJAX
只能同源使用的限制。
跨域时,浏览器会让请求带上Origin请求头,表明请求来自哪个站点;而服务器必须要让响应带上允许跨域访问的Access-Control-Allow-Origin响应头,表明允许某个站点可以进行访问该服务器。
浏览器将CORS请求分成两类:简单请求和非简单请求。怎么区分这两者呢?我们先来看两个条件:
(1)HTTP请求方法是以下三种之一:
·HEAD
·GET
·POST
(2)只包含简单HTTP请求头,即:
·Accept,
·Accept-Language,
·Content-Language,
·Content-Type并且值是 application/x-www-form-urlencoded, multipart/form-data, 或者 text/plain之一的(忽略参数)。
当请求满足上面的两个条件时,则该请求被视为简单请求,否则被视为非简单请求。简单请求与非简单请求的最主要区别就是跨域请求是否需要发送预检请求(preflight request)。
在进行跨域请求时,如果是简单请求,则浏览器会在请求中增加一个Origin请求头之后直接发送CORS
请求,服务器检查该请求头的值是否在服务器设置的CORS
许可范围内,如果在许可范围内,则服务器同意本次请求,如果不在许可范围内,则服务会返回一个没有包含Access-Control-Allow-Origin
响应头的HTTP
响应。值得注意的是,该响应的状态码还是200,所以无法通过状态码来识别是否跨域成功,但是浏览器在发现跨域请求的响应头没有包含 Access-Control-Allow-Origin
响应头时,就知道跨域失败了,这时浏览器会抛出一个错误,这个错误会被XMLHttpRequest
请求的onerror
回调函数捕获。下面两张图分别是在进行简单请求时,服务器允许跨域和不允许跨域时响应头的情况:
$("#visit").click(function () {
$.ajax({
url: 'http://localhost:8080/crossDomain/testCross',
contentType: "application/x-www-form-urlencoded",
type: 'get',
crossDomain:true,
success: function (res) {
console.log("哇哦,跨域成功了!")
console.log(res)
alert(JSON.stringify(res))
},
//跨域失败时,浏览器抛出的错误会被此回调函数捕获
error: function (e) {
console.log("哇哦,跨域失败了!")
console.log(e)
}
});
});
从代码可以看出,我们在跨域失败时浏览器的状态捕获了下来并将该错误在浏览器控制台打印了出来,下图是我们在浏览器控制台看到的输出信息,红色的是浏览器的报错信息,大概意思就是我们请求的资源没有Access-Control-Allow-Origin
响应头,所以这次XMLHttpRequest
请求被CORS
策略阻断了。此外我们也可以看到AJAX
的readyState
的值是0,表明浏览器认为这次跨域请求是没有发出去的(实际上已经请求成功了,但是浏览器阻止了JavaScript
读取访问到的内容)
如果是非简单请求,则浏览器会先发起一次预检请求(OPTIONS请求),浏览器除了会带上Origin请求头之外,还会再带上Access-Control-Request-Method 和 Access-Control-Request-Headers 这两个请求头,服务器在收到预检请求之后,会检查这三个请求头是否与服务器的资源设置(接口)一致,如服务器的接口只允许请求方法为GET
、Origin
为http://www.abc.com:8080
、Access-Control-Request-Headers
为 content-type
的请求,只要预检请求中三个请求头有任意一个值与服务器的资源(接口)设置不一致,服务器就会拒绝预检请求,如果都一致,则服务器确认通过预检请求并返回带有Access-Control-Allow-Credentials、Access-Control-Allow-Headers、Access-Control-Allow-Methods、Access-Control-Allow-Origin、Access-Control-Max-Age、Allow
等响应头的相应,这些响应头的作用可以看一下文章后面的附录,这里不再赘述。下面两张图分别是在进行预检请求时,服务器允许跨域和不允许跨域时响应头的情况:
如果预检请求通过,则浏览器会发送一个正常的和预检请求同名的跨域请求,如带自定义请求头的POST、GET的非简单请求或DELETE、PUT请求,否则返回的响应码为403,表示不允许请求,此时浏览器不会再发送同名的跨域请求。
我们还可以在有的浏览器中看到请求头带有Sec-Fetch-Mode和Sec-Fetch-Site这两个请求头,比如Chrome,这是用来标识此次请求的请求模式(跨域规则与浏览上下文)和是否跨域。
最后给出一张流程图:
前面提到过,同源策略是浏览器施加的安全限制,它只存在于浏览器中。因此,我们可以在前端服务器与后端服务器之间加一个代理中间件(比如Node中间件)来实现,通过代理中间件转发请求,从而达到跨域请求的目的。Node中间件代码如下:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.end.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.front.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.front.com' // 可以为false,表示不修改
}));
app.listen(3000);
前面我们提过,浏览器跨域访问js、css、img
等常规静态资源是被同源策略许可的,但iconfont
字体文件(eot|otf|ttf|woff|svg)
例外,这些文件是不会被允许的跨域访问的,此时可在Nginx
的静态资源服务器中加入以下配置。
location ~* \.(eot|ttf|woff|svg|otf)$ {
add_header Access-Control-Allow-Origin *;
}
或者是:
location / {
add_header Access-Control-Allow-Origin *;
}
我们还可以通过Nginx
配置一个代理服务器来转发请求,反向代理访问后台接口,并修改cookie中域名信息,从而实现跨域携带cookie。
server {
listen 80;
server_name www.front.com;
location / {
proxy_pass http://www.end.com:8080; #反向代理
proxy_cookie_domain www.end.com www.front.com; #将cookie里的域名修改为前端的域名
index index.html index.htm;
# 如果不是浏览器直接访问Nginx时,下面的跨域配置可不启用,下面配置是为了添加响应头
add_header Access-Control-Allow-Origin http://www.front.com; #当前端只进行跨域不需要携带cookie时,可为*,否则不能为*,具体看后面附录补充的请求头的说明
add_header Access-Control-Allow-Credentials true;
}
}
浏览器允许脚本直连一个WebSocket地址而不遵循同源策略,所以我们可以通过使用WebSocket协议来实现跨域。具体代码如下:
前端代码:
var socket = io('http://www.front.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
后台代码(NodeJS版,其他版本可以去百度或者Google找):
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
其他的跨域解决方案这里就不过多阐述了,这里给出相关的参考资料,感兴趣的可以看看。
postMessage
:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
Document.domain
:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain
localStorage和cookie的跨域解决方案
:https://www.cnblogs.com/vsmart/p/9388597.html
在AJAX
实际运行当中,对于访问XMLHttpRequest(XHR)
时并不是一次完成的,而是分别经历了多种状态后取得的结果,对于这种状态在AJAX
中共有5种,分别是:
0 - (未初始化)还没有调用send()
方法
1 - (载入)已调用send()
方法,正在发送请求
2 - (载入完成)send()
方法执行完成,
3 - (交互)正在解析响应内容
4 - (完成)响应内容解析完成,可以在客户端调用了
对于上面的状态,其中“0”状态是在定义后自动具有的状态值,而对于成功访问的状态(得到信息)我们大多数采用“4”进行判断。
1xx:请求收到,继续处理
2xx:操作成功收到,分析、接受
3xx:完成此请求必须进一步处理
4xx:请求包含一个错误语法或不能完成
5xx:服务器执行一个完全有效请求失败
100——客户必须继续发出请求
101——客户要求服务器根据请求转换HTTP
协议版本
200——交易成功
201——提示知道新文件的URL
202——接受和处理、但处理未完成
203——返回信息不确定或不完整
204——请求收到,但返回信息为空
205——服务器完成了请求,用户代理必须复位当前已经浏览过的文件
206——服务器已经完成了部分用户的GET
请求
300——请求的资源可在多处得到
301——删除请求数据
302——在其他地址发现了请求数据
303——建议客户访问其他URL
或访问方式
304——客户端已经执行了GET
,但文件未变化
305——请求的资源必须从服务器指定的地址得到
306——前一版本HTTP中使用的代码,现行版本中不再使用
307——申明请求的资源临时性删除
400——错误请求,如语法错误
401——请求授权失败
402——保留有效ChargeTo
头响应
403——请求不允许
404——没有发现文件、查询或URl
405——用户在Request-Line
字段定义的方法不允许
406——根据用户发送的Accept
拖,请求资源不可访问
407——类似401,用户必须首先在代理服务器上得到授权
408——客户端没有在用户指定的饿时间内完成请求
409——对当前资源状态,请求不能完成
410——服务器上不再有此资源且无进一步的参考地址
411——服务器拒绝用户定义的Content-Length
属性请求
412——一个或多个请求头字段在当前请求中错误
413——请求的资源大于服务器允许的大小
414——请求的资源URL长于服务器允许的长度
415——请求资源不支持请求项目格式
416——请求中包含Range
请求头字段,在当前请求资源范围内没有range
指示值,请求也不包含If-Range
请求头字段。
417——服务器不满足请求Expect
头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求。
500——服务器产生内部错误
501——服务器不支持请求的函数
502——服务器暂时不可用,有时是为了防止发生系统过载
503——服务器过载或暂停维修
504——关口过载,服务器使用另一个关口或服务来响应用户,等待时间设定值较长
505——服务器不支持或拒绝支请求头中指定的HTTP
版本
Origin: 存在于请求中**,**用于指明当前请求来自于哪个站点, Origin
仅仅包含站点信息,不包含任何路径信息。
Host:客户端指定自己想访问的HTTP
服务器的域名/IP
地址和端口号。
Referer: 当浏览器向web服务器发送请求的时候,一般会带上Referer
,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。
Access-Control-Allow-Origin: 它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求 。
Access-Control-Expose-Headers :CORS
请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。
Access-Control-Request-Method : 用来列出浏览器的CORS
请求会用到哪些HTTP方法 。
Access-Control-Request-Headers : 指定浏览器CORS
请求会额外发送的头信息字段。
Access-Control-Allow-Methods: 它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。
Access-Control-Allow-Headers: 表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Allow-Credentials: 它的值是一个布尔值,表示是否允许浏览器发送请求时带上Cookie
。默认情况下,Cookie
不包括在CORS
请求之中。设为true
,即表示服务器明确许可,Cookie
可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie
,删除该字段即可。 值得注意的是:Access-Control-Allow-Credentials为true时,Access-Control-Allow-Origin 的值不能为 *
Access-Control-Max-Age:用来指定本次预检请求的有效期,单位为秒。 非简单请求每次会发出两条请求,这样自然会影响我们的效率,HTTP
协议里面增加了一个响应头可以用来缓存我们的预检命令,这样在缓存有效内就不再需要发送预检请求了。
浏览器的同源策略:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
跨域资源共享:https://developer.mozilla.org/zh-CN/docs/Glossary/CORS
简单头部:https://developer.mozilla.org/zh-CN/docs/Glossary/%E7%AE%80%E5%8D%95%E5%A4%B4%E9%83%A8
预检请求:https://developer.mozilla.org/zh-CN/docs/Glossary/preflight_request
HTTP Headers:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers
HTTP访问控制(CORS):https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
postMessage:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
Document.domain:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain