Fellow Travellers

浅谈跨域——了解跨域的几种方式

陈伟红
字数统计: 10.9k阅读时长: 44 min
2018/11/19 Share

前言部分

跨域是什么?

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源(广义的)。

广义的跨域场景有以下几种:

  • 资源的跳转:A链接、重定向、表单提交;

  • 资源嵌入:<link>、<script>、<img>、<iframe> 等DOM标签,还有样式中 background:url()、@font-face()等文件外链;

  • 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等;

我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

同源策略是什么?

同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS(跨站脚本攻击)、CSFR(Cross-site request forgery 跨站请求伪造)等攻击。所谓同源是指 ”协议+域名+端口” 三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。

跨域的安全限制,主要是针对浏览器端来说的,服务器端是不存在跨域安全限制的。

浏览器的同源策略限制从一个源加载的文档或脚本与来自另一个源的资源进行交互。如果协议、端口和主机对于两个页面是相同的,则两个页面具有相同的源,否则就是不同源的。如果要在js里发起跨域请求,则要进行一些特殊处理了。或者,你可以把请求发到自己的服务端,再通过后台代码发起请求,再将数据返回前端。

同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取;
  • DOM 和 Js对象无法获得;
  • AJAX 请求不能发送;

常见的跨域场景有哪些?

URL 说明 是否允许通信
http://www.demo.com/a.js
http://www.demo.com/b.js
http://www.demo.com/lab/c.js
同一域名,不同文件或路径 允许
http://www.demo.com:8000/a.js
http://www.demo.com/b.js
同一域名,不同端口 不允许
http://www.demo.com/a.js
https://www.demo.com/b.js
同一域名,不同协议 不允许
http://www.demo.com/a.js
http://127.0.0.1/b.js
域名和域名对应相同ip 不允许
http://www.demo.com/a.js
http://x.demo.com/b.js
http://demo.com/c.js
主域相同,子域不同 不允许

了解更多:

  • 以上表格中第四个 “域名和域名对应相同IP” 不允许通信,虽然对应的IP地址是相同,但是也是不同的域名,这种也是判定为跨域的。我遇到的示例:做微信公众号的测试号配置时,需要在测试号的 体验接口权限表 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息 中配置 授权回调页面域名,这里配置的域名需要和提供公众号访问的域名保持一致,我理解的也就是保证非跨域情况配置。经过我多次踩坑,发现以下两种情况域名配置微信是无法进行授权回调的:

    1. 公司内网 IP 地址,经过外网映射之后形成的 IP 地址;
    2. IP 地址对应的一个域名;
    3. IP 地址相同,对应端口号未配置;

    第3条情况对应表格第二个“同一域名,不同端口”不允许访问,需要将端口严格保持一致。

    第1、2 条两种情况其实和表格中第四个的情况都是一回事,是属于 “域名和域名对应相同 IP” 的情况。后来我使用了内网的IP去设置,使授权回调IP和配置在公众号菜单访问的地址链接中的IP保持一致,完成了授权。

解决方案

  • 通过 jsonp 跨域

  • document.domain + iframe 跨域

  • location.hash + iframe

  • window.name + iframe 跨域

  • postMessage 跨域

  • 跨域资源共享(CORS)

  • nginx 代理跨域

  • nodejs 中间件代理跨域

  • WebSocket 协议跨域

跨域的几种解决方案

一、jsonp 方式

jsonp (json with padding) 是 json 的一种“使用模式”,是为了解决跨域问题而产生的解决方案。

jsonp 产生:

  1. AJAX 直接请求普通文件存在跨域无权限访问的问题, 尽管是静态页面;
  2. 我们在调用 js 文件的时候又不受跨域影响,比如引入 jquery 框架的时候;
  3. 凡是拥有 src 这个属性的标签都可以跨域例如 <script> <img> <iframe>;
  4. 如果想通过纯web端跨域访问数据只有一种可能,那就是把远程服务器上的数据装进js格式的文件里;
  5. json 是一个轻量级的数据格式,还被 js 原生支持;
  6. 为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback 参数给服务端;

举例:

以下是使用jQuery的jsonp发起跨域请求的例子描述,并总结。为了将过程描述清楚,内容过长,请谅解。

  • 我们先来看一看模拟静态文件访问跨域情况:
    只前端自己写一写代码,在不同端口形成的跨域情况下进行模拟;
    MacBook 的本地服务端口配置我将会在后续给出。
    ​ 1. 准备环境
    ​ Macbook users 路径下的站点,用自带Apache 配置一个 8001 端口,Apache默认端口 80 (/Library/WebServer/Document) 端口。端口不一样,构成跨域条件。

    2. 开始模拟,使用&lt;script&gt; 标签实现跨域访问
    
    • 测试文件准备
      端口为 *8001
      站点目录:
      8001站点目录.png
      代码如下, 文件名称写在代码顶部:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      <!--requestTest.html-->
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>跨域测试</title>
      <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
      <script type="text/javascript">
      // /*
      // 演示端口不同引起的跨域
      $(document).ready(function () {
      $("#btn").click(function () {
      $.ajax({
      // url: 'http://localhost:8001/student', // 默认的 *8001 端口站点, 数据所在位置,跨域条件达成
      // url: 'http://localhost:8080/message', // 自己配置的 users 下的 *8080 端口站点,数据所在位置,非跨域
      type: 'GET',
      success: function (data) {
      $(text).val(JSON.stringify(data))
      }
      })
      })
      })
      // */
      var message = function(data) {
      console.log('执行 message 方法.返回数据为:', data)
      alert(data[1].title)
      }
      </script>
      <!--使用 jsonp 实现跨域请求,用 js 包裹数据,可以访问。(这种情况更适用于前端的处理)-->
      <!--经测试,在未拼接 callback 的情况下,浏览器执行完这俩 js 文件,自动匹配了 message 方法。效果相同-->
      <script type="text/javascript" src="http://localhost:8080/message?callback=message"></script>
      </head>
      <body>
      <input id="btn" type="button" value="跨域获取数据">
      <textarea id="text" style="width: 400px; height: 100px;"></textarea>
      </body>
      </html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* student.json
*/
[{
"age": 22,
"birthday": "2018-1-10 12:12",
"id": 1,
"major": "信息管理",
"name": "Damon",
"status": true
}, {
"age": 23,
"birthday": "2018-1-10 12:12",
"id": 2,
"major": "软件工程",
"name": "John",
"status": true
}, {
"age": 24,
"birthday": "2018-1-10 12:12",
"id": 3,
"major": "计算机科学与技术",
"name": "Sonia",
"status": true
}, {
"age": 22,
"birthday": "2018-1-10 12:12",
"id": 4,
"major": "计算机科学与技术",
"name": "Mary",
"status": true
}]

端口为8080
站点目录:
8080站点目录.png
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* message.js
*/
console.log('服务器端执行前端传来的 message 方法。并携带参数返回。')
message([
{"id":"1", "title":"上海新闻联播,12岁的小王竟然比年仅六岁的小李大6岁!"},
{"id":"2", "title":"楼市告别富得流油 专家:房价下跌是大概率事件"},
{"id":"3", "title":"股市暴跌,双十一战绩赫然,这究竟是什么鬼迷了心窍?!"},
{"id":"4", "title":"没有运气,不要玩A股,你以为的谷底,只是下一个高地!"},
{"id":"5", "title":"美丽新世界,啦啦啦啦啦~"},
{"id":"6", "title":"国际要闻:听说昨天特朗普因为下个小雨没去开会!"},
{"id":"7", "title":"易烊千玺太帅,阿姨、妈妈、女友、姐姐组成的几千万粉丝高呼请他停止散发魅力!"},
{"id":"8", "title":"谢耳朵和艾米结婚了,好感动!"},
{"id":"9", "title":"如果明天不下雨,竟然也不一定看到太阳!"},
{"id":"10", "title":"没有新闻了。"}
]);

  • 演示
    在8001端口下的 requestTest.html 文件中,访问本端口映射的文件中的student文件,并展示在页面中,可正常访问到,结果如下:
    8001_student.png

在8001端口下 requestTest.html 文件中,访问 8080端口下的文件 message.js:

8001文件请求8080中message.png

该请求报错,提示跨域不被允许:
8001访问8080message报错信息.png

在8001端口下 requestTest.html 文件中,通过<script> 标签 包裹message.js 的请求:

8001通过script标签访问8080的message代码.png

可以看到跨域报错信息不见了,可以正常访问到数据:

8001 script方式跨域请求访问8080message运行结果.png

总结:能够正常访问数据,script 标签可以得到其他来源的数据,这也是jsonp的理论依据。缺点:只能进行get 请求,无法访问服务器的响应文本(单向请求)。

  • 现在来看jQuery 的 jsonp 方式跨域请求,结合后台服务器进行 jsonp 请求:
    这部分内容是后台小伙伴帮忙完成的,这里也非常感谢不辞劳苦,不厌其烦替我解答还帮我写demo的后台小伙伴!
    服务端代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    protected final static String CHARSET = ";charset=UTF-8";

    @RequestMapping(value = "/rest/public/weChat/subscription/queryAuthTaskStatus" ,method = RequestMethod.GET,produces = MediaType.APPLICATION_JSON_VALUE + CHARSET)
    @ResponseBody
    public void queryTaskStatus(HttpServletRequest request, PrintWriter out, HttpServletResponse response)throws ServletException, IOException {


    List<Student> studentList = new ArrayList();
    Student student = new Student();
    student.setName("Zhangshan");
    student.setAge("23");
    student.setMajor("前端html");
    student.setStatus(true);
    studentList.add(student);
    Student student2 = new Student();
    student2.setName("list");
    student2.setAge("20");
    student2.setMajor("java开发");
    student2.setStatus(true);
    studentList.add(student2);
    Student student3 = new Student();
    student3.setName("李明");
    student3.setAge("18");
    student3.setMajor("数据部门");
    student3.setStatus(false);
    studentList.add(student3);

    JSONArray jsonArray = JSONArray.fromObject(studentList);
    String result = jsonArray.toString();

    //前端传过来的回调函数名称
    String callback = request.getParameter("callback");


    //前端传过来的回调函数名称
    result = callback + "(" +result+")";
    response.getWriter().write(result);
    //out.write(result);

    //out.flush();
    //out.close();
    }
    }

我们首先看一下采用ajax 普通方式进行请求, js 代码如下:

1
2
3
4
5
6
7
8
9
$ajax({
url: "http://localhost:8082/SSM2/rest/public/weChat/subscription/query/queryAuthTaskStatus",
type: "get",
datatype: "json", // 指定服务器返回的数据类型
success: function(data) {
alert(asd);
// var asd = JSON.stringify(data);
}
});

普通方式请求提示跨域无法访问,结果如图:

jQuery普通方式请求服务器数据.jpg

然后我们更改代码,使用ajax jsonp方式请求,如果使用简单的方式,就只需配置 dataType: ‘jsonp’,就可以发起一个跨域请求。jsonp 指定服务器返回的数据类型为 jsonp 格式,可以看到请求的路径自动带了一个callback=xxx,xxx是 jQuery随机生成的一个回调函数名称。
服务器端代码不变,js 代码截图如下:

jQuery jsonp方式请求数据代码.png

可正常访问数据,请求结果如下(示例中json 数据解析出现乱码,暂时忽略):

jQuery jsonp 方式请求结果截图.jpg

以上的例子能简单看到 jsonp 是能够完成跨域请求的,结合前后台的配合,也更好理解怎么使用 jsonp 。

  • 我们再来简单看下如何指定 jsonp 回调函数:
    以下的例子和解说我搬运自:https://www.cnblogs.com/chiangchou/p/jsonp.html,看完醍醐灌顶。
    上代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
     1 <%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8"  language="java" %>
    2 <html>
    3 <head>
    4 <title>跨域测试</title>
    5 <script src="js/jquery-1.7.2.js"></script>
    6 <script>
    7
    8 function showData (data) {
    9 console.info("调用showData");
    10
    11 var result = JSON.stringify(data);
    12 $("#text").val(result);
    13 }
    14
    15 $(document).ready(function () {
    16
    17 // window.showData = function (data) {
    18 // console.info("调用showData");
    19 //
    20 // var result = JSON.stringify(data);
    21 // $("#text").val(result);
    22 // }
    23
    24 $("#btn").click(function () {
    25
    26 $.ajax({
    27 url: "http://localhost:9090/student",
    28 type: "GET",
    29 dataType: "jsonp", //指定服务器返回的数据类型
    30 jsonpCallback: "showData", //指定回调函数名称
    31 success: function (data) {
    32 console.info("调用success");
    33 }
    34 });
    35 });
    36
    37 });
    38 </script>
    39 </head>
    40 <body>
    41 <input id="btn" type="button" value="跨域获取数据" />
    42 <textarea id="text" style="width: 400px; height: 100px;"></textarea>
    43
    44 </body>
    45 </html>

回调函数可以写到<script>里(默认属于window对象),或者指明写到window对象里,看jQuery源码,可以看到jQuery调用回调函数时,是调用的window.callback。代码如上,看调用结果发现,请求时带的参数是callback=showData,然后再调用了success,后台先将参数放进回调函数里,本质上前端是插入了一个脚本,放入数据,就等于执行了那个回调方法,再进行网络请求的响应处理,所以回调函数先拿到了数据,success再走。success是返回成功以后必定会调的函数。

如果想要更改 callback 这个参数的名称,参考以下代码第23行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 1 <%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8"  language="java" %>
2 <html>
3 <head>
4 <title>跨域测试</title>
5 <script src="js/jquery-1.7.2.js"></script>
6 <script>
7
8 function showData (data) {
9 console.info("调用showData");
10
11 var result = JSON.stringify(data);
12 $("#text").val(result);
13 }
14
15 $(document).ready(function () {
16
17 $("#btn").click(function () {
18
19 $.ajax({
20 url: "http://localhost:9090/student",
21 type: "GET",
22 dataType: "jsonp", //指定服务器返回的数据类型
23 jsonp: "theFunction", //指定参数名称
24 jsonpCallback: "showData", //指定回调函数名称
25 success: function (data) {
26 console.info("调用success");
27 }
28 });
29 });
30
31 });
32 </script>
33 </head>
34 <body>
35 <input id="btn" type="button" value="跨域获取数据" />
36 <textarea id="text" style="width: 400px; height: 100px;"></textarea>
37
38 </body>
39 </html>

如此以来后台也要跟着改变,找到我前面的例子jQuery 的 jsonp 方式跨域请求中后台代码,找到下图对应位置并作出修改:

后台修改代码提示

把getParameter(“callback”)里的callback改成前面23行代码配置的函数名“theFunction”即可。

经测试,无法进行POST请求,要测试就将前面的请求方式更改成POST即可,结果如下:

jsonp进行POST请求报错信息

jsonp 本质就是执行了JavaScript。是通过 script 标签的开放策略,使网页可以获取其他来源的数据,用 jsonp 获取的数据也不是真正的 json,而是任意的JavaScript, 用JavaScript解释器,而不是用json解析器解析,ajax 只是对脚本请求做了封装。所以,ajax 的 jsonp 请求也是不支持 POST 的。在谷歌浏览器Chrome中查看 jsonp 发送的请求都是js类型,而不是 xhr【1,文末注解】 :【图片来源自水印地址】

请求类型示意图

总结

综上一大堆解释和示例,我们可以对 jsonp 原理作如下简单描述:
以下例子是较早学习跨域看到的描述,觉得写得很好,当时只记录了这个片段,未保存出处,若有人看到过望在评论指出,我会补上。也在此对作者表达歉意,并检讨以后摘录要记下出处

首先我们假设a网页调用b网站的服务

  1. a 网站需要准备一个方法,例如 callback(args);
  2. a 网站在页面中插入一个 <script> 标签,src 指向 b 网站的地址,并带上callback 作为参数;
  3. b网站接受请求处理后,把结果和回调方法的名字组成一个字符串返回,例如callback(‘data’);
  4. 由于是 script 标签,b 网站返回的字符串会被当成js解析执行,相当于调用到了 callback 方法;
  5. 主要利用了 script(img, iframe等有src属性的标签)可以跨站点访问的特性,且只能用 GET 请求,需要服务端做点配合,并且需要信任服务器(安全考虑)。jquery 的 jsonp ajax 只是封装了这个过程,让你看上去和普通 ajax 没什么区别,其实却一点关系都没有。

友情提示:axios 不支持 jsonp ,要用的话,可以装插件或者使用原生实现。

二、跨域资源共享

跨域资源共享(CORS)是一种网络浏览器的技术规范,它为web服务器定义了一种方式,允许网页从不同的域访问资源。CORS就是为了让AJAX可以实现可控的跨域访问而生的。

1、内部机制讲解

1) 简介

通过在HTTP Header中加入扩展字段,服务器在相应网页头部加入字段表示允许访问的domain和HTTP method,客户端检查自己的域是否在允许列表中,决定是否处理响应。

CORS 需要浏览器和服务器同时支持。目前所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

2) 两种请求

浏览器将 cors 请求分为两种请求:简单请求 和 非简单请求。简单请求就是使用设定的请求方式请求数据
而非简单请求则是在使用设定的请求方式请求数据之前,先发送一个OPTIONS请求,看服务端是否允许客户端发送非简单请求.只有”预检”通过后才会再发送一次请求用于数据传输。(例子,get、put 请求方式的区别)。

只要同时满足以下两大条件,就属于简单请求。

1
2
3
4
5
6
7
8
9
10
1) 请求方法是以下三种方法之一:
HEAD
GET
POST
2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

3) 简单请求:

浏览器对于简单的请求,直接发起 cors 请求。具体的说,就是在请求头加上 Origin 字段。比如截取规划小智请求的报头信息:(这个请求不是真的简单请求,只是找了实例为了方便查看结果关键的头信息)

1
2
3
4
5
6
7
8
9
10
11
12
Accept:*/*
Accept-Encoding:gzip, deflate, br
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Content-Length:630
content-type:application/json
Host:server.guihuaxiaozhi.com
Origin:http://127.0.0.1:33284
Pragma:no-cache
Referer:https://servicewechat.com/wx3e0ec267606ff974/devtools/page-frame.html
User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 wechatdevtools/1.02.1812180 MicroMessenger/6.7.3 Language/zh_CN webview/ token/9c8817cf1182bd6804aab2b7ad0a756a

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。(ps: 我理解具体就是请求的回应状态为200,但是会在控制台提示错误信息,access-origin-check)

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

1
2
3
4
5
6
7
8
9
10
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:http://127.0.0.1:33284
Access-Control-Expose-Headers:Set-Cookie
Connection:keep-alive
Content-Type:application/json;charset=UTF-8
Date:Thu, 27 Dec 2018 02:24:44 GMT
Server:nginx/1.10.2
Set-Cookie:SESSION=243e9e07-c7f9-4cb5-b896-e8807ecbb695; Path=/bdp-webconsumer/; HttpOnly
Transfer-Encoding:chunked
Vary:Origin

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。注意这几个是响应头里的,由服务器端设置。

  • Access-Control-Allow-Origin

    该字段是必须的。它的值要么是请求时Origin字段的值,指定域名的请求; 要么是一个*,表示接受任意域名的请求。

  • Access-Control-Allow-Credentials

    该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

  • Access-Control-Expose-Headers

    该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('Set-Cookie')可以返回Set-Cookie(服务器为浏览器设置cookie)字段的值。

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。另一方面,需要前端,在请求的时候设置withCredentials属性,才能让浏览器处理携带 cookie 发起请求。

注意:

如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

4)非简单请求
  • 预检请求

    非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

    浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

    下面是一段浏览器的JavaScript脚本。

    1
    2
    3
    4
    5
    6

    var url = 'http://api.alice.com/cors';
    var xhr = new XMLHttpRequest();
    xhr.open('PUT', url, true);
    xhr.setRequestHeader('X-Custom-Header', 'value');
    xhr.send();

    上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

    浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。

    1
    2
    3
    4
    5
    6
    7
    8
    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

    “预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

    除了Origin字段,”预检”请求的头信息包括两个特殊字段。

    • Access-Control-Request-Method

    该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

    • Access-Control-Request-Headers

    该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

  • 预检请求的回应(仍以上面的例子来说明)

    服务器收到”预检”请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain

    上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号(*),表示同意任意跨源请求。注意的点在简单请求里有提到,这里类似。

    1
    Access-Control-Allow-Origin: *

    如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

    1
    2
    3

    XMLHttpRequest cannot load http://api.alice.com.
    Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

    服务器回应的其他CORS相关字段如下。

    1
    2
    3
    4
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Allow-Credentials: true
    Access-Control-Max-Age: 1728000
    • Access-Control-Allow-Methods

    该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

    • Access-Control-Allow-Headers

    如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

    • Access-Control-Allow-Credentials

    该字段与简单请求时的含义相同。

    • Access-Control-Max-Age

    该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

  • 浏览器的正常请求和回应

    一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

    下面是”预检”请求之后,浏览器的正常CORS请求。

    1
    2
    3
    4
    5
    6
    7
    8
    > PUT /cors HTTP/1.1
    > Origin: http://api.bob.com
    > Host: api.alice.com
    > X-Custom-Header: value
    > Accept-Language: en-US
    > Connection: keep-alive
    > User-Agent: Mozilla/5.0...
    >

    上面头信息的Origin字段是浏览器自动添加的。

    下面是服务器正常的回应。

    1
    2
    3
    > Access-Control-Allow-Origin: http://api.bob.com
    > Content-Type: text/html; charset=utf-8
    >

    上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

如何实现 CORS

只需要在后台中加上配置来允许跨域请求。如果还需要携带cookie在前端被请求的Response header中加入允许携带配置,就可以实现跨域访问了!

以下也是后台小伙伴友情提供,这里再次感谢~(^__^) 嘻嘻……
下面我们来看一下 Java 的Tomcat 配置 cors:
首先需要下载 jar 包cors-filter与java-property-utils:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/com.thetransactioncompany/cors-filter -->
<dependency>
<groupId>com.thetransactioncompany</groupId>
<artifactId>cors-filter</artifactId>
<version>2.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.thetransactioncompany/java-property-utils -->
<dependency>
<groupId>com.thetransactioncompany</groupId>
<artifactId>java-property-utils</artifactId>
<version>1.10</version>
</dependency>

修改web.xml, 增加以下代码(最好放在其他filter前边)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<filter>         
<filter-name>CORS</filter-name>
<filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class>
<init-param>
<param-name>cors.allowOrigin</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.supportedMethods</param-name>
<param-value>GET, POST, HEAD, PUT, DELETE</param-value>
</init-param>
<init-param>
<param-name>cors.supportedHeaders</param-name>
<param-value>Accept, Origin, X-Requested-With, Content-Type, Last-Modified</param-value>
</init-param>
<init-param>
<param-name>cors.exposedHeaders</param-name>
<param-value>Set-Cookie</param-value>
</init-param>
<init-param>
<param-name>cors.supportsCredentials</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>CORS</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

jQuery请求示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.ajax("url", {
type: "POST",
xhrFields: {
withCredentials: true,
useDefaultXhrHeader: false
},
data: {
type: "test"
},
dataType: 'json',
crossDomain: true,
success: function(data, status, xhr) {
console.log(data);
}
});

跨域请求默认不会携带 cookie 信息,如果要携带请配置以下信息:

1
2
// 前端设置
“withCredentials”: true

Vue 框架 axios 配置:

1
axios.defaults.withCredentials = true

如果前端访问成功,则说明后台配置正确,反之,说明后台配置失败。

CORS 与 JSONP 的比较:

  • jsonp只能实现 GET 请求,而cors支持所有类型的http请求;

  • 使用cors,开发者可以使用XMLHttpRequest发起请求和获得响应,可以有更好的错误处理;

  • jsonp 主要被老的浏览器支持,但它们往往不支持cors,而绝大多数现代浏览器都已经支持了cors。

4、CORS主要应用场景:

  • 后台使用 restful API 架构,前后台不在同一服务器,需要用到。

三、nginx 反向代理接口跨域

有反向代理,那就肯定有正向代理。我们先来简单说说这个正、反向代理是个啥。

正向代理原理

正向代理类似一个跳板机,把浏览器访问过程委托给代理去做,代理访问外部资源。
正向代理原理示意图

举个例子:
我是一个用户,我访问不了某网站,但是我能访问一个代理服务器,这个代理服务器能访问那个我不能访问的网站,于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我。从目标网站的角度,只在代理服务器来取内容的时候有一次记录,有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站。
类似场景比如我们在外网去访问公司内网服务器B,我们先设置VPN,通过VPN将我们的请求转发到内网的A服务器,然后A把请求发到B上,响应内容返回到A,再由A通过VPN返回到我们。
工作流程可以描述为:
用户设置代理服务器,用户访问url,代理服务器代替用户访问并将网页内容返回。

反向代理服务器工作原理

反向代理(Reverse Proxy)方式是指后台内部网络服务器委托代理服务器,以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。
用户访问的是代理服务器,前端是不知道后台真实地址,只知道代理地址。
反向代理原理示意图

再举个栗子:
我是一个用户,我可以访问某一个网站,网站的数据是来源于我访问不到的内部网络上的内容服务器,内容服务器设置了可以访问自己的代理服务器。于是我向目标内容服务器发起请求,其实我访问的是内容服务器设置的代理服务器,这个代理服务器将我的请求转发到目标内容服务器上,获取到数据后再返回给网站上,我就可以看见了。
反向代理工作流程示意图
工作流程可以描述为:
和正向代理相反,由目标内容服务器设置代理服务器,代理转发用户发起的请求,获取数据再返回给用户。

使用 nginx 反向代理解决跨域

Nginx (engine x) 是一个高性能的HTTP反向代理服务,也是一个IMAP/POP3/SMTP服务。

我们前面提到跨域是浏览器的同源策略导致的,同源策略它是浏览器针对脚本攻击采取的一种安全策略,并不是 HTTP 协议的一部分。所以服务器端调用 HTTP 接口只是使用了 HTTP 协议,是不会执行 js 脚本的,不需要同源策略,也就不会形成跨域问题。
我们使用代理(同源)服务器发起请求,再由代理(同源)服务器请求内部服务器。

我们先来看看怎么来设置反向代理实现跨域请求。

  • 跨域举例
    假设有两个网站,A网站部署在:http://localhost:81 即本地ip端口81上;B网站部署在:http://localhost:82 即本地ip端口82上。现在A网站的页面想去访问B网站的信息,这时候浏览器是会报错的,因为形成了跨域。
    访问代码:

    1
    2
    3
    4
    5
    6
    7
    8
    <h2>Index</h2>
    <div id="show"></div>
    <script type="text/javascript">
    $(function () {
    $.get("http://localhost:82/api/values", {}, function (result) {
    $("#show").html(result);
    })
    })
  • nginx 搭建
    去官网下载 nginx,这个安装例子是Windows系统下的,下完然后安装。解压缩得到目录如下:
    nginx安装包解压缩目录

  • 配置 nginx.conf
    打开目录中的 “conf” 文件夹下的“nginx.conf”。以下为未修改的配置文件片段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #服务器的集群
    upstream rj.nginx.com { #服务器集群名字
    server 127.0.0.1:8001 weight=1;#服务器配置 weight是权重的意思,权重越大,分配的概率越大。
    server 127.0.0.1:8002 weight=2;
    }

    #当前的Nginx的配置
    server {
    listen 80; #监听80端口,可以改成其他端口
    server_name localhost; # 当前服务的域名

    #charset koi8-r;

    #access_log logs/host.access.log main;

    location / {
    proxy_pass http://rj.nginx.com;
    proxy_redirect default;
    }

修改后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80; #监听80端口,可以改成其他端口
server_name localhost; # 当前服务的域名

#charset koi8-r;

#access_log logs/host.access.log main;

#这里是单一代理,也可以配置多个,多代理
location /apis { #添加访问目录为/apis的代理配置
rewrite ^/apis/(.*)$ /$1 break;
proxy_pass http://localhost:82;
}
以下配置省略

如果要实现前端跨域携带cookie的则在 location中另外配置:

1
2
3
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;

如果是在浏览器中访问,项目访问地址需要和nginx代理同源,可以在同一个站点或者对nginx实现cors相关配置。
修改代码片段解析:

  1. 由配置信息可知,我们让nginx监听localhost的80端口,网站A与网站B的访问都是经过localhost的80端口进行访问;
  2. 我们特殊配置了一个“/apis”目录的访问,并且对url执行了重写,最后使以“/apis”开头的地址(访问时的地址)都转到“http://localhost:82”(目标服务器地址)进行处理;
  3. rewrite ^/apis/(.)$ /$1 break;
    rewrite代表重写拦截进来的请求,并且只能对域名后边以“/apis”开头的起作用,例如www.a.com/apis/msg?x=1重写。只对/apis重写。
    rewrite后面的参数是一个简单的正则 ^/apis/(.
    )$ ,$1代表正则中的第一个(),$2代表第二个()的值,以此类推。
    break代表匹配一个之后停止匹配。
  • 访问地址修改
    配置了nginx,那么所有的访问都要走nginx,而不是走网站原本的地址(A网站localhost:81,B网站localhost:82)。所以要修改A网站中的ajax访问地址,把访问地址由“ http://localhost:82/api/values” 改成 “/apis/api/values”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <h2>Index</h2>
    <div id="show"></div>

    <script type="text/javascript">
    $(function () {
    $.get("/apis/api/values", {}, function (result) {
    $("#show").html(result);
    })
    })
    </script>

然后在浏览器中访问B的数据就可以成功获取了。

四、nodejs 中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过开启一个代理服务器(同源),实现数据的转发。
由于我所试验 nodejs 跨域的例子是已有的Vue项目,配置代理后实现了跨域获取数据,下面的例子分“非 Vue”和“Vue”两种进行说明。

  1. 非 Vue 框架的跨域
    利用node + express + http-proxy-middleware搭建一个proxy服务器。
  • 安装 node 环境、 express、 http-proxy-middleware

    express是基于 Node.js 平台,快速、开放、极简的 web 开发框架。
    http-proxy-middleware 是专门用于 http 代理的一个 node 中间件,适用于connect, express, browser-sync 等等,由热门的http-proxy 驱动。

  • 新建 js 文件: nodeProxy.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    "use strict";
    const express = require('express');
    const path = require('path');
    const app = express();
    const request = require('request');

    // 配置静态文件服务中间件
    let serverUrl='http://192.168.1.220:8080'; // 目标后端服务地址
    app.use(express.static(path.join(__dirname, './'))); //静态资源 index.html 和node代码在一个目录下
    app.use('/', function(req, res) {
    let url = serverUrl + req.url; // req.url 传入的接口路径
    req.pipe(request(url)).pipe(res);
    });

    app.listen(3000,'127.0.0.1', function () {//前端 ajax 地址写 http://127.0.0.1:3000/
    console.log('server is running at port 3000'); // 3000为将要启动的端口
    });
  • 运行 node nodeProxy.js
    node 做代理转发请求服务器,可以跨域请求数据。

  1. Vue 框架的跨域
    利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
  • 安装中间件 http-proxy-middleware 和 express,已经有的话就不必装了。
    使用命令:

    npm install –save-dev express http-proxy-middleware

  • webpack文件配置
    我使用的vue-cli, webpack 版本为 3.8.1。
    找到项目中config文件夹,然后打开index.js, 看到如下片段,进行配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    module.exports = {
    dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
    '/gxtz-server-web/': {
    tartget: 'http://192.168.1.220',
    changeOrigin: true,
    pathRewrite: {'^/api/': '/'}
    }
    },

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined

    /**
    * Source Maps
    */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: '#source-map',

    #其他省略
    },
    }

配置相关属性含义和前面 nginx 中相同。这样就可以进行代理了。

五、WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
关于WebSocket我没还没有自己做实例去验证跨域,但是WebSocket本身支持跨域,只要会使用就行,之前写小程序使用过,但是代码封装程度高不适合作为例子看。以下例子是从前端常见跨域解决方案(全) 摘抄来的,可以参考一下。

  • 前端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <div>user input:<input type="text"></div>
    <script src="./socket.io.js"></script>
    <script>
    var socket = io('http://www.domain2.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);
    };
    </script>
  • Nodejs socket后台:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var http = require('http');
    var socket = require('socket.io');

    // 启http服务
    var server = http.createServer(function(req, res) {
    res.writeHead(200, {
    'Content-type': 'text/html'
    });
    res.end();
    });

    server.listen('8080');
    console.log('Server is running at port 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.');
    });
    });

结尾

简单的总结概括:

  • jsonp
    适用于访问接口get请求返回值是json类型,又是跨域的情况。
    1)只支持 get 请求;
    2)需要后端配合,能够返回 callback 返回希望得到的数据;
    3)存在一定的安全性问题,不能防止滥用跨域请求的非法网站恶意调用。使用的话需做好安全防范;

  • cors
    由于CORS是W3C中一项较“新”的方案,目前,浏览器支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS),CORS也已经成为主流的跨域解决方案。
    1)纯后端设置,无需前端做处理,如果需要携带cookie,前后端都要配置,前端需要配置”withCredetails”: “true;

  • 代理
    1)nginx 反向代理,一般适用于外网访问不了的内部网络请求,做反向代理来获取数据;
    2)nodejs 中间件代理,前后端分离引起跨域问题(从原理看nginx 反向代理应该也适用),开发阶段前后台不同源的情况。

其他的没有详细描述。在使用过程中就能够知晓很明确的使用场景,后续我再陆续补充。

#参考链接
前端常见跨域解决方案(全)
jQuery jsonp跨域请求

script 标签加载文件

浏览器同源政策及其规避方法-阮一峰

cors跨域解析-阮一峰

http header 的一些含义

还有关于 iframe 等相关的可以参考阮一峰的 浏览器同源政策及其规避方法

如果有没理解的也可以去这些博客找找灵感~~


【注解1】使用XMLHttpRequest (XHR)对象可以与服务器交互。您可以从URL获取数据,而无需让整个的页面刷新。这使得Web页面可以只更新页面的局部,而不影响用户的操作。

尽管名称如此,XMLHttpRequest可以用于获取任何类型的数据,而不仅仅是XML,它还支持 HTTP以外的协议(包括文件和ftp)。

【注解2】前端将回调函数传到后台,后台将要返回的数据绑定到回调函数上,这时候前端的回调中就可以拿到数据了,在后台执行响应response的相关语句后在前端的请求success中才会拿到。

1
2
result = callback + "(" +result+")"; // 绑定前端传入回调函数
response.getWriter().write(result); // 后台将数据作为响应带回。不加这一句前端指定的callback对应函数能拿到数据,但是ajax请求success中是获取不到数据的。

对于更详细的XMLHttpRequest请求内容可以参考:使用XMLHttpRequest


CATALOG
  1. 1. 前言部分
    1. 1.1. 跨域是什么?
    2. 1.2. 同源策略是什么?
    3. 1.3. 常见的跨域场景有哪些?
      1. 1.3.0.1. 了解更多:
  2. 1.4. 解决方案
  • 2. 跨域的几种解决方案
    1. 2.1. 一、jsonp 方式
      1. 2.1.0.1. jsonp 产生:
      2. 2.1.0.2. 举例:
      3. 2.1.0.3. 总结
  • 2.2. 二、跨域资源共享
    1. 2.2.0.1. 1、内部机制讲解
      1. 2.2.0.1.0.1. 1) 简介
      2. 2.2.0.1.0.2. 2) 两种请求
      3. 2.2.0.1.0.3. 3) 简单请求:
      4. 2.2.0.1.0.4. 4)非简单请求
  • 2.2.0.2. 如何实现 CORS
  • 2.2.0.3. CORS 与 JSONP 的比较:
  • 2.2.0.4. 4、CORS主要应用场景:
  • 2.3. 三、nginx 反向代理接口跨域
    1. 2.3.1. 正向代理原理
    2. 2.3.2. 反向代理服务器工作原理
    3. 2.3.3. 使用 nginx 反向代理解决跨域
  • 2.4. 四、nodejs 中间件代理跨域
  • 2.5. 五、WebSocket协议跨域
  • 3. 结尾