深入浅出FE(三)跨域Cross-Origin

1,686 阅读27分钟

目录

1. 跨域是什么

2. 为什么有跨域

2.1 防止csrf攻击

2.2 防止xss攻击

3. 跨域解决方案?

3.1 jsonp

3.2 "跨域资源共享"(Cross-origin resource sharing)CROS

3.3 document.domain + iframe跨域

3.4 window.name + iframe

3.5 location.hash + iframe跨域

3.6 window.postmessage

3.7 websocket

3.8 nginx代理(来自前端常见跨域解决方案(全))

3.9 中间件代理(其他语言同理)

3.10 Comet

3.11 服务器发送事件(SSE)

4、拓展

5、参考资料

1. 跨域是什么

介绍跨域之前,先来了解同源策略。同源策略就是浏览器为了保证用户信息的安全,防止恶意的网站窃取数据,禁止不同域之间的JS进行交互。对于浏览器而言只要域名、协议和端口其中一个不同就会引发同源策略,从而限制他们之间如下的交互行为。

浏览器的同源策略会导致跨域,即两个页面只要域名、协议和端口三者任何一个不一致,请求都会跨域。

2. 为什么有跨域

跨域是为了防止csrf和xss攻击。

2.1 防止csrf攻击

(1)当用户访问银行www.bank.com,登陆并操作,这时用户凭证如cookie等都生成并存放在浏览器;

(2)此时用户又访问了另一个钓鱼网站,这时该钓鱼网站网站就可以在它的页面中,拿到银行的cookie,比如用户名,登陆token等,然后发起对www.bank.com的操作;

(3)如果这时浏览器如果没有同源策略,银行也没有做响应的安全处理,那么用户就会被攻击。

2.2 防止xss攻击

xss攻击包括三种类型-存储型、反射型和DOM型,这里主要是DOM型攻击。

(1)有攻击者制作了一个钓鱼网站,这个网站内部嵌了一个和www.bank.com外观一致的iframe;

(2)此时用户进来后忽视了url不是银行的的页面,直接输入账户名和密码;

(3)如果这时浏览器如果没有同源策略,那么这些敏感信息就会被黑客获取,银行也没有做相应的处理,那么用户就会被攻击。

3. 跨域解决方案?

3.1 jsonp

3.1.1 原理

jsonp 是一种数据调用的方式。利用<script>标签没有跨域限制的“漏洞”。

3.1.2 用法

主要有两种方式,一种是写在url中,或者是动态生成script标签,并插入到dom中,

当需要和后端通讯时,本站脚本创建一个<script>元素,地址指向第三方的API网址,形如:

写在url中

<script src="http://querydata.com/jsonp/api/defalt?callback=querydata;"></script>

动态生成script,插入到dom中

// 创建一个脚本,并且告诉后端回调函数名叫querydata
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.type = 'text/javasctipt';
script.src = 'demo.js?callback=querydata';
body.appendChild(script);


在本地提供一个回调函数querydata来接收数据(函数名可约定,或通过地址参数传递)。

function querydata(data){
    console.log(data)
}

产生的响应为json数据的包装(故称之为jsonp,即json padding),形如:

callback({"name":"coolsummer","gender":"Male"})

这样浏览器会调用querydata函数,并传递解析后json对象作为参数。本站脚本可在callback函数里处理所传入的数据。

3.1.3 优缺点及使用场景
3.1.3.1 jsonp 只能使用get请求,不能用于其他类型的请求。

3.1.3.2 没有关于 JSONP 调用的错误处理。

如果动态脚本插入有效,就执行调用;如果无效,就静默失败。失败是没有任何提示的。例如,不能从服务器捕捉到 404 错误,也不能取消或重新开始请求。不过,等待一段时间还没有响应的话,就不用理它了。(未来的 jQuery 版本可能有终止 JSONP 请求的特性)。

3.1.3.3 JSONP 被不信任的服务使用时会很危险。

因为 JSONP 服务返回打包在函数调用中的 JSON 响应,而函数调用是由浏览器执行的,这使宿主 Web 应用程序更容易受到各类攻击。如果打算使用 JSONP 服务,了解它能造成的威胁非常重要。

3.1.3.4 拓展(来自:jsonp的工作原理):

  • html标签的src属性没有同源限制(支持跨域),浏览器解析script标签时,会自动下载src属性值(url)指向的资源;
  • script标签指向的资源文件被下载后,其中的内容会被立即执行
  • 服务器端的程序会解析src属性值中的url传递的参数,根据这些参数针对性返回一个/多个函数调用表达式,这些函数调用表达式的参数就是客户端跨域想得到的数据
  • 服务器生成、返回的文件中,表达式调用的函数是已经在本地提前定义好的,而参数就是希望从跨域服务器拿到的数据。
  • 字面的script标签可以,动态添加到dom树中的script也可以,后者更方便绑定事件。

所以只要拥有”src”这个属性的标签都拥有跨域的能力,类似的可以跨域内嵌资源的还有:

(1)<script src=""></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。上面jsonp也用到了呢。

(2) <link src="">标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari (跳至CVE-2010-0051)部分 和 Opera。

(3)<video> 和 <audio>嵌入多媒体资源。

(4)<object>, <embed> 和 <applet>的插件。

(5)@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。

(6) <frame> 和 <iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。

(7) image

图像ping是与服务器进行简单、单向的跨域通信的一种方式,请求的数据是通过查询字符串的形式发送的,而响应可以是任意内容,但通常是像素图或204相应(No Content)。 图像ping有两个主要缺点:首先就是只能发送get请求,其次就是无法访问服务器的响应文本。

var img = new Image();
img.onload = img.onerror = function(){
    alert("done!");
};
img.src = "跨域资源路径";
document.body.insertBefore(img,document.body.firstChild);

3.2 "跨域资源共享"(Cross-origin resource sharing)CROS

3.2.1 使用场景及优缺点

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS),CORS也已经成为主流的跨域解决方案。

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

CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

3.2.2 原理

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

MDN解释:

跨域资源共享(CORS)是一种机制,该机制使用附加的HTTP标头来告诉浏览器以使Web应用程序在一个来源运行,并从另一个来源访问选定的资源。Web应用程序请求其来源(域,协议或端口)不同的资源时,将执行跨域HTTP请求

3.2.3 用法

实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。普通跨域请求只服务端设置Access-Control-Allow-Origin即可,前端无须设置.

若要带cookie请求:前后端都需要设置。由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。

浏览器请求分为两种:简单请求和非简单请求。

3.2.3.1 简单请求

对于简单请求,浏览器直接发出CORS请求。

具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。并且origin中的值必须在后端设置的Access-Control-Allow-Origin的值中,如果后端设置了Access-Control-Allow-Origin这个属性为‘*’,即origin可以为任意值。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

1)当origin不是服务端设置的Access-Control-Allow-Origin属性中的值,服务器会返回一个正常的http响应,这时浏览器发现返回的response中没有包含Access-Control-Allow-Origin字段,那么会抛出错误,这个错误可以被xhr的onerror回调函数捕获,但是这个错误无法通过http状态码识别,因为可能状态码为200.

2)当origin是服务端设置的Access-Control-Allow-Origin属性的值(或者Access-Control-Allow-Origin属性设置为*),服务器会多返回几个和跨域相关的字段:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin 服务端设置的Access-Control-Allow-Origin属性的值(或者Access-Control-Allow-Origin属性设置为*,即允许所有的请求,这样通常会导致更多的麻烦,比如对接口的攻击等,一般设置为指定的域);
  • Access-Control-Allow-Credentials 可选布尔值,服务端是否发送cookie,默认情况下,Cookie不包括在CORS请求之中,即此值为false;此时如果想要在请求中带上cookie,那么前端需要设置带上cookie的字段withCredentials字段。

以xhr、ajax或者axios请求为例:

xhr

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

ajax

$.ajax({
        url: "http://localhost:9000",
        type: 'GET',
        xhrFields: {
            withCredentials: true // 这里设置了withCredentials
        },
        success: function(data) {
            console.log(data)
        },
        error: function(err) {
            console.error(err)
        }
    })

axios

axios.defaults.withCredentials=true

服务端也要设置这个属性为true,不同语言设置方式各有不同,在此不一一举例

Access-Control-Allow-Credentials: true

注意:前端和服务端必须同时设置,否则两端任意一端设置,另一端不设置则不会携带cokie。但是省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

xhr.withCredentials = false;
  • Access-Control-Expose-Headers 字段可选。

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

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

简单请求是满足以下所有条件的请求(以下内容来自MDN:跨域资源共享(CORS),稍有修改):

注意:这些与Web内容已经可以发出的跨站点请求种类相同,除非服务器发送适当的标头,否则不会向请求者释放响应数据。因此,可以防止跨站点请求伪造的站点不必担心HTTP访问控制。

注: WebKit每日和Safari浏览器技术预览放置在允许的值的额外限制AcceptAccept-LanguageContent-Language头。如果这些标头中的任何一个具有“非标准”值,则WebKit / Safari不会将请求视为“简单请求”。没有记录WebKit / Safari认为“非标准”的值,以下WebKit错误除外:

没有其他浏览器实现这些额外的限制,因为它们不是规范的一部分。

3.2.3.2 非简单请求

非简单请求会在简单请求发送之前,增加一次“预检”请求。

“简单请求”即不需要“预检”,“预检”请求首先通过OPTIONS方法将HTTP请求发送到另一个域上的资源,以确定实际请求是否可以安全发送。跨站点请求这样被预检,因为它们可能会影响用户数据。

浏览器在发送请求前只要不是简单请求,就会先发一个预检请求检测请求是否被服务器接受,比如域名是否在服务器所允许的白名单中,以及可以使用哪些HTTP动词和头信息字段。

一个常见的预检请求头如下所示:

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字段,"预检"请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method告知服务器实际请求所使用的 HTTP 方法。
  • Access-Control-Request-Headers告知服务器实际请求所携带的自定义首部字段。该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段

服务器基于从预检请求获得的信息来判断,是否接受接下来的实际请求。

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

//来自MDN
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2019 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

上面一段代码是常见的预检请求返回内容,最关键是Access-Control-Allow-Origin这个字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

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

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

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

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-Method字段区别,请求的Access-Control-Allow-Method字段是单个的,表示档次请求的方法,是单个方法,而服务端响应的Access-Control-Allow-Methods返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
  • Access-Control-Allow-Headers如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
  • Access-Control-Allow-Credentials该字段与简单请求时的含义相同,是否携带cookie。
  • Access-Control-Max-Age该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

服务端通过预检请求后:

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

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

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字段是浏览器自动添加的。

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

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

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

3.3 document.domain + iframe跨域

3.3.1 使用场景及优缺点

对于主域名相同,而子域名不同的情况,可以使用document.domain来跨域 这种方式非常适用于iframe跨域的情况。

缺点是只能用于iframe,而且如果两个页面通信必须通过第三个页面,并且传递值只能是单向。

MDN解释:

Document 接口的 domain 属性获取/设置当前文档的原始域部分,常用于同源策略

3.3.2 原理

两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

3.3.3 用法(来自前端常见跨域解决方案(全)

1.)父窗口:(www.a.com/a.html)

<iframe id="iframe" src="http://child.a.com/b.html"></iframe>
<script>
    document.domain = 'a.com';
    var user = 'admin';
</script>

2.)子窗口:(child.a.com/b.html)

<script>
    document.domain = 'a.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

3.4 window.name + iframe

3.4.1 使用场景及优缺点

浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。并且可以支持非常长的 name 值(2MB)。

这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

3.4.2 原理

通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

3.4.3 用法

父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name属性。

window.name = data;

接着,子窗口跳回一个与主窗口同域的网址。

location = 'http://parent.url.com/xxx.html';

然后,主窗口就可以读取子窗口的window.name了。

var data = document.getElementById('myFrame').contentWindow.name;

3.5 location.hash + iframe跨域

3.5.1 实现原理及优缺点

a与b跨域相互通信,通过中间页c来实现(且c与a是同域)。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

3.5.2 用法

A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

嵌套关系为: a.html中嵌套iframe(b.html),b.html中嵌套iframe(c.html),c中可以获得a.html的对象。

1.)a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hashsetTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2.)b.html:(www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3.)c.html:(www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

3.6 window.postmessage

3.6.1 使用场景和优缺点

a) 页面和其打开的新窗口的数据传递

b) 多窗口之间消息传递

c) 页面与嵌套的iframe消息传递

d) 上面三个场景的跨域数据传递

优点是公共的标准,使用简单,缺点是用于接收消息的页面可能存在跨站安全问题,因为无法检查origin和source属性会导致跨站点脚本攻击。

这个应该就是以后解决dom跨域通用方法了

3.6.2 原理

API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

3.6.3 用法(来自MDN)

/*
 * A窗口的域名是<http://example.com:8080>,以下是A窗口的script标签下的代码:
 */

var popup = window.open(...popup details...);

// 如果弹出框没有被阻止且加载完成

// 这行语句没有发送信息出去,即使假设当前页面没有改变location(因为targetOrigin设置不对)
popup.postMessage("The user is 'bob' and the password is 'secret'",
                  "https://secure.example.net");

// 假设当前页面没有改变location,这条语句会成功添加message到发送队列中去(targetOrigin设置对了)
popup.postMessage("hello there!", "http://example.org");

function receiveMessage(event)
{
  // 我们能相信信息的发送者吗?  (也许这个发送者和我们最初打开的不是同一个页面).
  if (event.origin !== "http://example.org")
    return;

  // event.source 是我们通过window.open打开的弹出页面 popup
  // event.data 是 popup发送给当前页面的消息 "hi there yourself!  the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
/*
 * 弹出页 popup 域名是<http://example.org>,以下是script标签中的代码:
 */

//当A页面postMessage被调用后,这个function被addEventListenner调用
function receiveMessage(event)
{
  // 我们能信任信息来源吗?
  if (event.origin !== "http://example.com:8080")
    return;

  // event.source 就当前弹出页的来源页面
  // event.data 是 "hello there!"

  // 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把event.source
  // 作为回信的对象,并且把event.origin作为targetOrigin
  event.source.postMessage("hi there yourself!  the secret response " +
                           "is: rheeeeet!",
                           event.origin);
}

window.addEventListener("message", receiveMessage, false);

注意:任何窗口可以在任何其他窗口访问此方法,在任何时间,无论文档在窗口中的位置,向其发送消息。 因此,用于接收消息的任何事件监听器必须首先使用origin和source属性来检查消息的发送者的身份。 这不能低估:无法检查origin和source属性会导致跨站点脚本攻击。

3.7 websocket

3.7.1 应用场景及优缺点

我们知道websocket是H5为了解决http协议单项请求的缺陷。虽然可以用轮训解决-每隔一段时间查询状态变化,但是这种方式存在延时且对服务端造成很大负载。

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

websocket最大的特点就是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

websocket的特点:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

3.7.2 原理

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

3.7.3 用法

如下是websocket的一个简单的用法

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};      

原生WebSocket API使用起来不太方便,可以使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。本此因为是模拟就没有安装了用了WebSocket。

3.8 nginx代理(来自前端常见跨域解决方案(全))

1、 nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

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

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

3.9 中间件代理(其他语言同理)

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

3.10 Comet

Comet是一种服务器向页面推送数据的技术。有两种实现方式:长轮询和流。

3.10.1 长轮询

传统轮询(短轮询)是指浏览器定时向服务器发送请求,看有没有更新的数据。而长轮询是短轮询的翻版:页面发起一个到服务器的请求,服务器一直保持连接打开,直到有数据可发送,发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开关闭过程中一直持续不断。

无论是长轮询还是短轮询,浏览器都要在接收数据之前,先发起对服务器的连接。长轮询和短轮询最大的区别是在与服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。

3.10.2 流

Http流不同于长轮询和短轮询,它在页面的整个生命周期中只使用一个http连接。具体就是服务器保持连接,周期性的向浏览器发送数据。

在各种浏览器中,通过侦听readystatechange事件及监听readystate的值是否为3,就可以利用XHR对象是想HTTP流。

function createStreamingClient(url, progress, finished){
    var xhr = new XMLHttpRequest(),
        received = 0;
    xhr.open("get, url, true");
    xhr.onreadystatechange = function(){
        var result;
        if(xhr.readyState ==3){
            //只取得最新数据并调整计数器
            result = xhr.responseText.substring(received);
            received += result.length;
            //调用progress回调函数
            progress(result);
        } else if (xhr.readyState ==4){
            finished(xhr.response);
        }
    };
    xhr.send(null);
    return xhr;
}
var client = createStreamingClient("streaming.php", function(data){
    alert("Received: " + data);
}, function(data){
    alert("Done!");
});

这个例子能在大多数浏览器中正常运行(IE除外),但管理Comet连接很容易出错,需要时间不断改进才能达到完美。

3.11 服务器发送事件(SSE)

SSE(Server-Sent Event)是围绕只读Comet交互推出的API或者模式。

3.11.1 SEE API

SSE API用于创建到服务器的单向连接,服务器通过这个链接可以发送任意数量的数据。服务器相应的MIME必须是text/event-stream,而且是浏览器中的JavaScript API能解析格式输出,SSE支持短轮询、长轮询和HTTP流,而且能在断开连接时自动确定何时重新连接。

var source = new EventSource("入口点")//相当于一个接口

注意传入的URL必须要和创建对象的页面同源,EventSource的实例有一个readyState属性,值为0表示正连接到服务器,1代表打开了连接,2代表关闭连接。

还有三个事件:open(建立连接时触发)、message(从服务器收到新事件时触发)和error(无法建立连接时触发)。

一般用法:

source.onmessage = function(event){
    var data = event.data;
    //process data...

}

默认情况下,EventSource对象会保持与服务器的活动连接,如果连接断开,还会重新连接,这就意味这个SSE适合长轮询和HTTP流。如果想强制立即断开连接并且不再打开,可以调用close()方法。

3.11.2 事件流

所谓的服务器事件会通过一个持久的HTTP响应发送,这个相应的MIME类型为text/event-stream.相应的文本格式是纯文本,最简单的情况是每个数据项都带有前缀data:,并且每个data:数据项后面有空行才会触发message事件,因此服务器在生成每个事件流时不能忘记添加空行。

//事件流,每行数据后都有一个空行,意味着每一行数据都会触发message事件
data: aaa
data: bbb
data: ccc

通过设置id:前缀可以给特定的事件制定一个关联的ID,这个ID位于data:行前后都可以。

data: aaa
id:1
data: bbb
data: ccc

通过设置id后,EventSource对象会跟踪上一次触发的事件,如果连接断开,会向服务器发送一个包含名为Last-Event-ID的特殊HTTP头部的请求,以便服务器知道下一次该触发哪个事件。在多次的连续事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。

4、拓展

1.注意本文主要讨论前端的CROS,关于服务端的CROS可参考:服务器端访问控制(CORS)

2.通过离线缓存,如localhost和indexdb也能够跨域

3.阮一峰文章浏览器同源政策及其规避方法中提到的片段识别码是一种不太常见的方式。

5、参考资料

1.跨域资源共享 CORS 详解

2.jsonp的工作原理

3.结合 JSONP 和 jQuery 快速构建强大的 mashup

4.跨域资源共享 CORS 详解

5.跨域资源共享(CORS)

6.Document.domain

7.前端常见跨域解决方案(全)

8.SendMessage、PostMessage原理

9.WebSocket 教程

10.浏览器同源政策及其规避方法