跨域问题

为什么会有跨域问题

确保Web安全是十分重要的,避免数据窃取和受到不必要的攻击。所以浏览器中的同源策略,限制了一个源(origin)中加载的文档或脚本与其他源(origin)中的资源交互的方式,是为了隔离潜在恶意文档的关键安全机制,减少可能被攻击的媒介,是重要的安全策略

同源:如果两个 URL 的 protocolport (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

同源

跨源网络访问

不同源之间的交互类型:

  • 跨域写入:一般是被允许的。例如链接(links),重定向以及表单提交。特定少数的HTTP请求需要添加 preflight
  • 跨域资源嵌入:一般是被允许。例如<srcipt>标签嵌入跨域脚本;<link>标签嵌入CSS,因CSS松散的语法规则跨域需要设置HTTP头部Content-Typeimg / video / audio / 插件 / @font-face都可以通过标签引入不同源的资源;iframe标签可以载入任何资源,站点可以设置 X-Frame-Options 阻止跨域,以防点击劫持攻击
  • 跨域读操作:一般是不被允许的,但常可以通过内嵌资源来巧妙的进行读取访问。例如,你可以读取嵌入图片的高度和宽度,调用内嵌脚本的方法,或availability of an embedded resource.

阻止跨源访问

  • 阻止跨域写操作:检测请求中的CSRF Token,使用这个标记来阻止页面的跨站读操作。

  • 阻止资源的跨站读取:需要保证该资源是不可嵌入的。阻止嵌入行为是必须的,因为嵌入资源通常向其暴露信息。

  • 阻止跨站嵌入:可以使用CSRF Token来防止嵌入。

csrf - Cross Site Request Forgery - 跨站请求伪造。CSRF Token = Token

解决跨域问题方案

一般是解决客户端XMLHttpRequest对象与服务器之间的数据交互的跨源问题。

应用场景较多的时候是前后端分离后联调进行数据交互,大部分时候还是不要开启可跨域方案。

JSONP

参考链接:https://www.runoob.com/json/json-jsonp.html

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。

JSONPjson的一种“使用模式”,需要服务端将传输数据包装成JSONP的数据格式,前端可以设置ajax响应数据类型dataTypeJSONP类型。

仅支持get方法的请求具有局限性,不安全可能会遭受XSS攻击。

JSONP的实现流程

  • 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
  • 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
  • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show('我不爱你')
  • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

CORS

参考链接:跨域资源共享 CORS 详解 – 阮一峰

CORS需要浏览器和服务器同时支持。目前,绝大部分浏览器都支持该功能。

简单请求

满足两大条件就是简单请求:

  • 请求方法是HEADGETPOST这三种。
  • HTTP头信息不超出AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于application/x-www-form-unlencodedmultipart/form-datatext/plain)这几种字段。

同时满足以上两个条件的就是简单请求,否则就是非简单请求。

浏览器发出CORS简单请求

具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

服务端如果需要CORS请求包含cookie,则必须在AJAX请求中打开withCredentials属性。

服务器处理CORS简单请求

服务器需要设置响应HTTP头

Access-Control-Allow-Origin: http://api.bob.com -- 必须,设置*表示接受任意域名的请求
Access-Control-Allow-Credentials: true -- 可选,表示是否需要cookie,默认情况下cookie不包括在CORS请求中
Access-Control-Expose-Headers: FooBar -- 可选,表示服务端想要获取的Header的字段

如果需要发送Cookie,Access-Control-Allow-Origin就不能设置通配符,必须要指定明确的、与请求网页一致的域名。

非简单请求

不属于简单请求的请求,就是非简单请求。

如请求方式是PUTDELETE等,Content-Type字段类型是application/json等。

浏览器发送CORS非简单请求

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。会发送正式数据交互之前,发送一个OPTIONS方式的请求,对服务器进行询问当前域名是否在服务器的许可名单之中、可以使用哪些请求方式和头信息字段等。只有服务端回复后,如果不行就直接报错,如果可以浏览器才会发出正式的请求。

OPTIONS请求头会包含询问字段

Origin: http://api.bob.com -- 表示请求来自哪个源
Access-Control-Request-Method: PUT -- 必须,表示接下来的操作
Access-Control-Request-Headers: X-Custom-Header -- 可选,多个值用逗号隔开,表示会额外发送的头信息字段
服务器处理CORS非简单请求

服务器对预检请求OPTIONS做回应

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT -- 必须,表明服务器支持的所有跨域请求的方法,为了避免多次预检
Access-Control-Allow-Headers: X-Custom-Header -- 与Access-Control-Request-Headers对应,如果请求中有则响应中也必须有
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000 -- 可选,指定本次预检的有效期,如果没有过期再次请求就不会进行预检

在部分与CSRF有关的请求中,请求的Header中会携带Origin字段。字段内包含请求的域名(不包含path及query)。

如果Origin存在,那么直接使用Origin中的字段确认来源域名就可以。

但是Origin在以下两种情况下并不存在:

  • IE11同源策略: IE 11 不会在跨站CORS请求上添加Origin标头,Referer头将仍然是唯一的标识。最根本原因是因为IE 11对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考MDN Same-origin_policy#IE_Exceptions
  • 302重定向: 在302重定向之后Origin不包含在重定向的请求中,因为Origin可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的URL,因此浏览器不想将Origin泄漏到新的服务器上。

解决方案:根据HTTP协议,在HTTP头中有一个Referer字段,记录了该HTTP请求的来源地址。 对于Ajax请求,图片和script等资源请求,Referer为发起请求的页面地址。对于页面跳转,Referer为打开页面历史记录的前一个页面地址。因此我们使用Referer中链接的Origin部分可以得知请求的来源域名。

Referrer Policy字段的策略设置成same-origin,对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer。

以上相关内容参考链接:https://tech.meituan.com/2018/10/11/fe-security-csrf.html

服务器转发

前端一般常用Node进行服务器代理中间件。原理与其他服务器相同。

同源策略仅在客户端生效,所以服务器可以进行多个源数据的请求。

  • 接受同源的客户端请求。
  • 将请求转发给目标服务器。
  • 拿到目标服务器的数据返回。
  • 将目标服务器的数据返回给同源的客户端。
  • 不一定必须同源,可以是中间转发服务器设置CORS。

前端常用方法,使用node环境下的http-server实现请求转发。

webpack中的devServer.proxy就是使用webpack-dev-server起一个本地服务器进行代理。

Nginx反向代理

需要设置nginx的服务器设置。nginx接受到了客户端的请求,直接转发到目标服务器,目标服务器返回的数据又会被代理返回客户端。

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

// 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;
    }
}

window.postMessage

可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议,端口号,以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage()方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

可以用于解决的问题:

  • 页面和其打开的新窗口的数据传递(window.open() / window.opener
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

部分小程序框架的web-viewh5页面进行通信使用的就是window.postMessage()

WebSocket

WebsocketHTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。

WebSocketHTTP都是应用层协议,都基于 TCP 协议。

WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

iframe

window.name:窗口的名字主要用于为超链接和表单设置目标(targets)。窗口不需要有名称。

location.hash

<a id="myAnchor" href="/en-US/docs/Location.href#Examples">Examples</a>
<script>
  var anchor = document.getElementById("myAnchor");
  console.log(anchor.hash); // 返回'#Examples'
</script>

总结

  • CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案。
  • JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
  • 服务器转发和nginx反向代理,主要是通过同源策略对服务器不加限制。
  • 前后端生产环境下常用的跨域方案是corsnginx反向代理。

参考链接

不要再问我跨域的问题了 – segmentfault

浏览器的同源策略 – MDN

九种跨域方式实现原理(完整版)– 掘金