参考: 犀牛书-18.1.6(pg503) 红宝书-21-4
受浏览器同源策略的影响, ajax存在跨域限制,所以出现多种解决方式来跳过这种限制
安全-同源策略
目前有几种类型的解决方式:
1 借助不存在跨域限制的DOM元素,如img or script等,常见于老项目维护
2 浏览器和服务器之间协商判断,既CORS,目前新项目采用的主流方案
3 通过代理服务转发绕过浏览器的同源策略,比如常见的webpack proxy, 常见于开发阶段

下面具体分析:

1 图像ping

使用img标签,可以从任何网页中加载图像,无跨域限制。
而且其可以动态创建,设置src那一刻,请求就开始发送了,并且可以通过load 或 error 事件来判断成功or失败

  1. var img = new Image();
  2. img.onload = img.onerror = function(){
  3. alert("Done!");
  4. };
  5. img.src = "https://www.ga.com/test.html?sum=a";

but 两个缺点:
响应可以是任何内容,但是img标签无对应API来访问服务器的响应,所以只能是单向通信
img标签只能是get请求

所以,这种方式通常用在跟踪用户点击页面 或 动态广告曝光次数
比如项目中的页面埋点方案(借鉴阿里spm,上传 站点a-页面b-模块c-商品d 等信息)就是通过该方式向统计服务器发送信息。
当页面加载完毕,发送pv等信息
当模块商品加载完毕,发送商品曝光等信息
当用户点击,发送用户操作信息
image.png

  1. var s = new Image();
  2. s.onload = function () {};
  3. s.src =
  4. "https://d1.xxx.com/track/tracklog.jsp?v=0.0.1&tacktype=web&site=www&status=true&data=" +
  5. encodeURIComponent(JSON.stringify(t));

参考:
埋点

2 jsonp(json with padding)

利用script标签,设置src,插入doucument中,就会发送一个请求
它不受同源策略影响
自动执行响应体(包含json数据,数据被包裹在一个函数中,如下图所示)

  1. showResult(["customername1","customername2"])

由两个部分组成:回调函数 和 数据
回调函数:是当响应到来时应该调用的函数,一般要在请求中指定,并且对于通用的支持jsonp的接口服务,这个 请求参数key就是callback
参数值value既函数名可以是前端提前写好的函数名,如果使用工具库如$.jsonp,则由库来处理(jquey 后期版本支持自定义)
http://www.othersite.com/demo5.ashx?callback=showResult
数据:既传入到函数中的接口真正提供的json数据

优点:相对于img,jsonp方式可以直接访问响应文本,支持双向通信
缺点:从其他域获取数据,可能不安全
H5给script标签新增onerror事件,用于判断请求是否失败,但浏览器支持度不一

完整例子:

  1. <script>
  2. function showResult(json){
  3. var obj= document.getElementById("result");
  4. obj.innerHTML="天气:"+json.weather+";风力:"+json.wind;
  5. }
  6. var script=document.createElement("script");
  7. script.src="http://www.othersite.com/demo5.ashx?callback=showResult";
  8. document.body.insertBefore(script,document.body.firstChild);
  9. </script>

工具库

  1. $.ajax({
  2. type: "get",
  3. url: "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998",
  4. dataType: "jsonp",
  5. jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)
  6. jsonpCallback:"flightHandler",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
  7. success: function(json){
  8. alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。');
  9. },
  10. error: function(){
  11. alert('fail');
  12. }
  13. });

3 CORS

cross-origin resource sharing — 跨域资源共享
原理:当访问跨域资源时,客户端(浏览器)和 服务端之间通过某些http头部字段来沟通,从而决定请求或响应是应该成功,还是应该失败。
浏览器发送请求是会额外附加一个origin头部,包含页面来源(协议+域名+端口)
Origin: https://developer.mozilla.org
image.png
服务器会根据这个头部信息决定是否给与响应,如果服务器认定该请求可以接受,就会在设置一个响应头Access-Control-Allow-Origin
image.png
Access-Control-Allow-Origin: https://developer.mozilla.org
如果是个公共资源,则可以直接设置Access-Control-Allow-Origin: *
实际上存在多个响应头,看一个真实CORS请求:
image.png

实际上CORS还有更多知识点,比如请求凭证、简单请求、复杂请求、预检请求等知识点

3.1 简单请求

image.png

3.2 复杂请求

简单请求以外的都是复杂请求,需要先发送 预检请求(prefilght).
要求必须首先使用 [OPTIONS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONS) 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
比如:

  1. var invocation = new XMLHttpRequest();
  2. var url = 'http://bar.other/resources/post-here/';
  3. var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
  4. function callOtherDomain(){
  5. if(invocation)
  6. {
  7. invocation.open('POST', url, true);
  8. invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
  9. invocation.setRequestHeader('Content-Type', 'application/xml');
  10. invocation.onreadystatechange = handler;
  11. invocation.send(body);
  12. }
  13. }

上面的代码使用 POST 请求发送一个 XML 文档,该请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong)。另外,该请求的 Content-Type 为 application/xml。因此,该请求需要首先发起“预检请求”。
检请求中同时携带了下面两个首部字段:

  1. Access-Control-Request-Method: POST
  2. 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-PINGOTHERContent

完整的预检请求和响应报文

  1. OPTIONS /resources/post-here/ HTTP/1.1
  2. Host: bar.other
  3. User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  5. Accept-Language: en-us,en;q=0.5
  6. Accept-Encoding: gzip,deflate
  7. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
  8. Connection: keep-alive
  9. Origin: http://foo.example
  10. Access-Control-Request-Method: POST
  11. Access-Control-Request-Headers: X-PINGOTHER, Content-Type
  12. HTTP/1.1 200 OK
  13. Date: Mon, 01 Dec 2008 01:15:39 GMT
  14. Server: Apache/2.0.61 (Unix)
  15. Access-Control-Allow-Origin: http://foo.example
  16. Access-Control-Allow-Methods: POST, GET, OPTIONS
  17. Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  18. Access-Control-Max-Age: 86400
  19. Vary: Accept-Encoding, Origin
  20. Content-Encoding: gzip
  21. Content-Length: 0
  22. Keep-Alive: timeout=2, max=100
  23. Connection: Keep-Alive
  24. Content-Type: text/plain
  1. POST /resources/post-here/ HTTP/1.1
  2. Host: bar.other
  3. User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  5. Accept-Language: en-us,en;q=0.5
  6. Accept-Encoding: gzip,deflate
  7. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
  8. Connection: keep-alive
  9. X-PINGOTHER: pingpong
  10. Content-Type: text/xml; charset=UTF-8
  11. Referer: http://foo.example/examples/preflightInvocation.html
  12. Content-Length: 55
  13. Origin: http://foo.example
  14. Pragma: no-cache
  15. Cache-Control: no-cache
  16. <?xml version="1.0"?><person><name>Arun</name></person>
  17. HTTP/1.1 200 OK
  18. Date: Mon, 01 Dec 2008 01:15:40 GMT
  19. Server: Apache/2.0.61 (Unix)
  20. Access-Control-Allow-Origin: http://foo.example
  21. Vary: Accept-Encoding, Origin
  22. Content-Encoding: gzip
  23. Content-Length: 235
  24. Keep-Alive: timeout=2, max=99
  25. Connection: Keep-Alive
  26. Content-Type: text/plain
  27. [Some GZIP'd payload]

3.3 附带身份凭证的请求

跨域请求通常不会包含任何的用户证书:cookie 和 HTTP身份验证令牌(token)。 服务端设置的cookie也会被丢弃。
如果要发送凭证信息,需要设置 XMLHttpRequest 的withCredentials为true。

  1. var xhr = new XMLHttpRequest();
  2. xhr.open('GET', url, true);
  3. xhr.withCredentials = true;
  4. xhr.onreadystatechange = handler;
  5. xhr.send();

在$.ajax
image.png
在aixose

  1. axios.defaults.withCredentials = true;//允许跨域携带cookie信息

但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。
image.png

3.3.1 实际案例

某个需求,页面加了个底部固定的登录模块,需要分流展示,前端是根据后端接口(接口域名固定www.xxx.com)返回的分流字段判断是否展示,后端又是根据cookie中某个字段判断。
网站是跨境电商平台,除了www主站,还存在10几个多语站点,比如 fr.xxx.com de.xxx.com
提测后,测试反馈,主站正常,到了多语站点,cookie符合分流条件,但是接口分流字段永远返回false。
经过排除,多语站点(fr.xxx.com),接口请求(www.xxx.com)没有携带cookie导致
image.png
加上以后完美解决

3.4 注意响应头设置重复也会报错

cors响应头设置重复也会导致出错,The ‘Access-Control-Allow-Origin’ header contains multiple values ‘, ‘, but only one is allowed 错误。
某次上线后,服务端设置过了容许跨域,nginx作为反向代理,也设置了容许跨域,导致报错
image.png

image.png

4 代理服务转发

前端经常在本地开发时用的127.0.0.1:3000这样的开发地址,开发阶段就存在跨域。
但是同源策略只是浏览器的安全限制,那完全本地可以起一个node服务来做转发代理
比如vue-cli或create-react-app这样的脚手架都已经在webpack中集成这样的功能
vue开发跨域解决办法
如果正好是做node全栈类的项目,就可以写个路由转发

  1. 前端ajax请求
  2. //本地调试跨域问题(发布需要注释掉)
  3. //url = '/n/api/proxy?proxys='+ encodeURIComponent(url);
  1. node
  2. router.get('/n/api/proxy',controller.api.api.index)
  3. const { ctx, service } = this
  4. let queryObj = ctx.query;
  5. let proxys = queryObj.proxys;
  6. delete queryObj.proxys
  7. if(proxys){
  8. const option = {
  9. url:decodeURIComponent(proxys),
  10. type:"GET",
  11. data: queryObj || {},
  12. dataType: 'json',
  13. timeout: '10000',
  14. headers: ctx.header,
  15. }
  16. const res = await service.curl.ajax(option);
  17. console.log(res.data,4444)
  18. ctx.body = res.data

5 access-control-allow-origin插件

这个插件在本地开发时完全不用管跨域,但是千万别忘了确认上线后是否存在跨域
不支持本地localhost
参考