Mmear's 😘.

前端跨域问题及解决方案

字数统计: 1.9k阅读时长: 7 min
2019/03/11 Share

前端跨域问题

什么是跨域

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

  • 资源的跳转:如 <a>标签超链接,重定向,表单提交等;
  • 资源的嵌入:<link>、<script>、<img>、<iframe>标签等;
  • 脚本请求:通过 JS 发起的 Ajax 请求,DOM 和 JS 对象的跨域操作灯等;

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

同源策略(Same origin policy)

同源策略是浏览器最基本的安全策略,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 攻击;同源是指:如果协议、主机、端口对于两个页面是相同的,则两个页面是同源的;
下表是给出了对http://store.company.com:80的同源检测:
| URL | 结果 | 原因 |
| — | — | — |
|http://store.company.com/dir/inner/another.html | 成功 | 同一域名 |
|http://store.company.com/dir2/other.html | 成功 | 同一域名下不同文件夹 |
|https://store.company.com/secure.html | 失败 | 不同的协议 ( https ) |
|http://store.company.com:81/dir/etc.html| 失败 | 不同的端口 ( 81 ) |
|http://news.company.com/dir/other.html | 失败 | 主域相同,子域不同( news ) |
|http://news.company.com/dir/other.html | 失败 | 域名不同 |

解决方案

主域相同的跨域

document.domain + iframe 实现跨域

完全不同源的跨域(两个页面之间的通信)

  1. 通过location.hash跨域
  2. 通过window.name跨域
  3. HTML5 通过引入全新的跨文档通信 APIwindow.postMessage 允许跨文档通信
    Paste_Image.png

AJAX 请求不同源的跨域

JSONP

通过 JSONP请求跨域,网页通过添加一个脚本元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来:

1
2
3
4
5
6
7
8
/* client */
// 回调函数
function cb (data) {
console.log(data);
}
const script = document.createElement('script');
// 向服务器发送请求
script.src = "/http://server.test/data?callback=cb";

服务器返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* server */
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;

// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');

res.end();
});

server.listen('8080');

WebSocket

通过WebSocket跨域,是一种通信协议,不执行同源政策;

跨域资源共享(CORS):

当一个资源请求一个其它域名的资源时会发起一个跨域HTTP请求(cross-origin HTTP request)。比如说,域名A的某 Web 应用通过<img> 标签引入了域名B的某图片资源,域名A的 Web 应用就会导致浏览器发起一个跨域 HTTP 请求。在当今的 Web 开发中,使用跨域 HTTP 请求加载各类资源(包括CSS、图片、JavaScript 脚本以及其它类资源),已经成为了一种普遍且流行的方式。

众所周知,出于安全考虑,浏览器会拦截跨域请求的返回结果。比如,使用XMLHttpRequest对象Fetch发起 HTTP 请求就必须遵守『同源策略』。 具体而言,Web 应用程序通过XMLHttpRequest对象或Fetch能且只能向同域名的资源发起 HTTP 请求,而不能向任何其它域名发起请求。
img

简单请求

所谓的简单,是指:

  • 只使用 GET, HEAD 或者POST请求方法。如果使用POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种;
  • 不会使用自定义请求头(类似于 X-Modified );

cors_1.png
浏览器在发送简单跨域请求时需加上origin请求头,标明站点来源;同时服务器需设置Access-Control-Allow-Origin响应头为『*』或指明的站点地址;

预请求

不同于上面讨论的简单请求,『预请求』要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏(POST 请求不具有幂等性)。 当请求具备以下条件,就会被当成预请求处理:

  • 请求以 GET, HEAD 或者 POST 以外的方法发起请求。者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml的 XML 数据;
  • 使用自请求头(比如添加诸如 X-PINGOTHER)
1
2
3
4
5
6
7
8
9
10
11
12
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = 'some messages';
function callOtherDomain(){
if(invocation){
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong'); // 自定义请求头
invocation.setRequestHeader('Content-Type', 'application/xml'); // 发送XML数据
invocation.onreadystatechange = handler;
invocation.send(body);
}
}

如上,以 XMLHttpRequest 创建了一个 POST 请求,为该请求添加了一个自定义请求头(X-PINGOTHER: pingpong),并指定数据类型为 application/xml。所以,该请求是一个『预请求』形式的跨站请求。浏览器使用一个 OPTIONS 发送了一个『预请求』。Firefox 3.1 根据请求参数,决定需要发送一个『预请求』,来探明服务器端是否接受后续真正的请求。 OPTIONS 是 HTTP/1.1 里的方法,用来获取更多服务器端的信息,是一个不应该对服务器数据造成影响的方法。 随同 OPTIONS 请求,以下两个请求头一起被发送:

1
2
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

假设服务器成功响应返回部分信息如下:

1
2
3
4
5
6
7
8
 //表明服务器允许http://foo.example的请求
Access-Control-Allow-Origin: http://foo.example
//表明服务器可以接受POST, GET和 OPTIONS的请求方法
Access-Control-Allow-Methods: POST, GET, OPTIONS
//传递一个可接受的自定义请求头列表。服务器也需要设置一个与浏览器对应。否则会报 Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response 的错误
Access-Control-Allow-Headers: X-PINGOTHER
//告诉浏览器,本次“预请求”的响应结果有效时间是多久。在上面的例子里,1728000秒代表着20天内,浏览器在处理针对该服务器的跨站请求,都可以无需再发送“预请求”,只需根据本次结果进行判断处理。
Access-Control-Max-Age: 1728000
附带凭证信息的请求

XMLHttpRequest 和访问控制功能中最有趣的特性就是,发送凭证请求(HTTP Cookies和验证信息)的功能。一般而言,对于跨站请求,浏览器是不会发送凭证信息的。但如果将 XMLHttpRequest 的一个特殊标志位withCredentials设置为true,浏览器就将在请求中附带凭证信息。

1
2
3
4
5
6
7
8
9
10
11
//http://foo.example站点的脚本向http://bar.other站点发送一个GET请求,并设置了一个Cookies值
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}

假设服务器成功响应返回部分信息如下:

1
2
3
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT // 创建了更多的cookie信息

如果服务器端的响应中,如果没有返回Access-Control-Allow-Credentials: true的响应头,那么浏览器将不会把响应结果传递给发出请求的脚本程序,以保证信息的安全。同时Access-Control-Allow-Origin必须设置为请求传递的Origin,否则响应会失败;

CORS的优点:CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案

参考链接
CATALOG
  1. 1. 前端跨域问题
    1. 1.1. 什么是跨域
    2. 1.2. 同源策略(Same origin policy)
    3. 1.3. 解决方案
      1. 1.3.1. 主域相同的跨域
      2. 1.3.2. 完全不同源的跨域(两个页面之间的通信)
      3. 1.3.3. AJAX 请求不同源的跨域
        1. 1.3.3.1. JSONP
        2. 1.3.3.2. WebSocket
        3. 1.3.3.3. 跨域资源共享(CORS):
          1. 1.3.3.3.1. 简单请求
          2. 1.3.3.3.2. 预请求
          3. 1.3.3.3.3. 附带凭证信息的请求
            1. 1.3.3.3.3.1. 参考链接