参考: 犀牛书-18.1.6(pg503) 红宝书-21-4
受浏览器同源策略的影响, ajax存在跨域限制,所以出现多种解决方式来跳过这种限制
安全-同源策略
目前有几种类型的解决方式:
1 借助不存在跨域限制的DOM元素,如img or script等,常见于老项目维护
2 浏览器和服务器之间协商判断,既CORS,目前新项目采用的主流方案
3 通过代理服务转发绕过浏览器的同源策略,比如常见的webpack proxy, 常见于开发阶段
1 图像ping
使用img标签,可以从任何网页中加载图像,无跨域限制。
而且其可以动态创建,设置src那一刻,请求就开始发送了,并且可以通过load 或 error 事件来判断成功or失败
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "https://www.ga.com/test.html?sum=a";
but 两个缺点:
响应可以是任何内容,但是img标签无对应API来访问服务器的响应,所以只能是单向通信
img标签只能是get请求
所以,这种方式通常用在跟踪用户点击页面 或 动态广告曝光次数
比如项目中的页面埋点方案(借鉴阿里spm,上传 站点a-页面b-模块c-商品d 等信息)就是通过该方式向统计服务器发送信息。
当页面加载完毕,发送pv等信息
当模块商品加载完毕,发送商品曝光等信息
当用户点击,发送用户操作信息
var s = new Image();
s.onload = function () {};
s.src =
"https://d1.xxx.com/track/tracklog.jsp?v=0.0.1&tacktype=web&site=www&status=true&data=" +
encodeURIComponent(JSON.stringify(t));
参考:
埋点
2 jsonp(json with padding)
利用script标签,设置src,插入doucument中,就会发送一个请求
它不受同源策略影响
自动执行响应体(包含json数据,数据被包裹在一个函数中,如下图所示)
showResult(["customername1","customername2"])
由两个部分组成:回调函数 和 数据
回调函数:是当响应到来时应该调用的函数,一般要在请求中指定,并且对于通用的支持jsonp的接口服务,这个 请求参数key就是callback
参数值value既函数名可以是前端提前写好的函数名,如果使用工具库如$.jsonp,则由库来处理(jquey 后期版本支持自定义)
http://www.othersite.com/demo5.ashx?callback=showResult
数据:既传入到函数中的接口真正提供的json数据
优点:相对于img,jsonp方式可以直接访问响应文本,支持双向通信
缺点:从其他域获取数据,可能不安全
H5给script标签新增onerror事件,用于判断请求是否失败,但浏览器支持度不一
完整例子:
<script>
function showResult(json){
var obj= document.getElementById("result");
obj.innerHTML="天气:"+json.weather+";风力:"+json.wind;
}
var script=document.createElement("script");
script.src="http://www.othersite.com/demo5.ashx?callback=showResult";
document.body.insertBefore(script,document.body.firstChild);
</script>
工具库
$.ajax({
type: "get",
url: "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998",
dataType: "jsonp",
jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)
jsonpCallback:"flightHandler",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
success: function(json){
alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。');
},
error: function(){
alert('fail');
}
});
3 CORS
cross-origin resource sharing — 跨域资源共享
原理:当访问跨域资源时,客户端(浏览器)和 服务端之间通过某些http头部字段来沟通,从而决定请求或响应是应该成功,还是应该失败。
浏览器发送请求是会额外附加一个origin头部,包含页面来源(协议+域名+端口)
Origin: https://developer.mozilla.org
服务器会根据这个头部信息决定是否给与响应,如果服务器认定该请求可以接受,就会在设置一个响应头Access-Control-Allow-Origin
Access-Control-Allow-Origin: https://developer.mozilla.org
如果是个公共资源,则可以直接设置Access-Control-Allow-Origin: *
实际上存在多个响应头,看一个真实CORS请求:
实际上CORS还有更多知识点,比如请求凭证、简单请求、复杂请求、预检请求等知识点
3.1 简单请求
3.2 复杂请求
简单请求以外的都是复杂请求,需要先发送 预检请求(prefilght).
要求必须首先使用 [OPTIONS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONS)
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
比如:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
上面的代码使用 POST 请求发送一个 XML 文档,该请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong)。另外,该请求的 Content-Type 为 application/xml。因此,该请求需要首先发起“预检请求”。
检请求中同时携带了下面两个首部字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
首部字段 [Access-Control-Request-Method](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Method)
告知服务器,实际请求将使用 POST 方法。首部字段 [Access-Control-Request-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Headers)
告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER
与 Content
完整的预检请求和响应报文
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache
<?xml version="1.0"?><person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
3.3 附带身份凭证的请求
跨域请求通常不会包含任何的用户证书:cookie 和 HTTP身份验证令牌(token)。 服务端设置的cookie也会被丢弃。
如果要发送凭证信息,需要设置 XMLHttpRequest 的withCredentials为true。
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.withCredentials = true;
xhr.onreadystatechange = handler;
xhr.send();
在$.ajax
在aixose
axios.defaults.withCredentials = true;//允许跨域携带cookie信息
但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。
3.3.1 实际案例
某个需求,页面加了个底部固定的登录模块,需要分流展示,前端是根据后端接口(接口域名固定www.xxx.com)返回的分流字段判断是否展示,后端又是根据cookie中某个字段判断。
网站是跨境电商平台,除了www主站,还存在10几个多语站点,比如 fr.xxx.com de.xxx.com
提测后,测试反馈,主站正常,到了多语站点,cookie符合分流条件,但是接口分流字段永远返回false。
经过排除,多语站点(fr.xxx.com),接口请求(www.xxx.com)没有携带cookie导致
加上以后完美解决
3.4 注意响应头设置重复也会报错
cors响应头设置重复也会导致出错,The ‘Access-Control-Allow-Origin’ header contains multiple values ‘, ‘, but only one is allowed 错误。
某次上线后,服务端设置过了容许跨域,nginx作为反向代理,也设置了容许跨域,导致报错
4 代理服务转发
前端经常在本地开发时用的127.0.0.1:3000这样的开发地址,开发阶段就存在跨域。
但是同源策略只是浏览器的安全限制,那完全本地可以起一个node服务来做转发代理
比如vue-cli或create-react-app这样的脚手架都已经在webpack中集成这样的功能
vue开发跨域解决办法
如果正好是做node全栈类的项目,就可以写个路由转发
前端ajax请求
//本地调试跨域问题(发布需要注释掉)
//url = '/n/api/proxy?proxys='+ encodeURIComponent(url);
node端
router.get('/n/api/proxy',controller.api.api.index)
const { ctx, service } = this
let queryObj = ctx.query;
let proxys = queryObj.proxys;
delete queryObj.proxys
if(proxys){
const option = {
url:decodeURIComponent(proxys),
type:"GET",
data: queryObj || {},
dataType: 'json',
timeout: '10000',
headers: ctx.header,
}
const res = await service.curl.ajax(option);
console.log(res.data,4444)
ctx.body = res.data
5 access-control-allow-origin插件
这个插件在本地开发时完全不用管跨域,但是千万别忘了确认上线后是否存在跨域
不支持本地localhost
参考