总结:

浏览器无同源限制,则有问题:CSRF

浏览器有同源限制,有时需要绕过限制,如何实现?反向代理、JSONP、CORS

JSONP的弊端:XSS

除了浏览器同源限制,后端确认前端请求是否合法的方式:origin字段、https、wss等

 

跨域

跨域是指从一个域名的网页去访问另一个域名的网页。一个完整URL地址通常由 协议+主机+端口+路径[+search][+hash] 组成,其中search和hash是可选项(search也称query,hash也称fragment)、协议未列出则默认为http、port未列出则默认为80(http)或443(https)值。因此严格来说,两个网页地址的协议(http/https)、主机、端口三者任何一个不同就可以认为是跨域访问,即使两个地址是不同子域名也是跨域,如app.baidu.com与baidu.com。

 

同源策略

浏览器出于安全考虑(防止CSRF)会限制跨域访问(即同源策略,Same Origin Policy,SOP)。在该限制下,除非两个网页是来自于同一‘源头’, 否则不允许一个网页的JavaScript访问另外一个网页的内容,像Cookie,DOM,LocalStorage均禁止访问;但对具有src属性的标签(如script、img、iframe等)不做跨域限制。

 

浏览器跨域访问资源的三种类型

跨域读操作(Cross-origin reads):一般是不被允许的,受同源策略限制。通常的http 接口请求属于此类型

跨域写操作(Cross-origin writes):一般是被允许的。如重定向、表单提交(如form表单的提交)。

跨域资源嵌入(Cross-origin embedding):一般是允许的。包括:

a标签链接
<script src="..."></script>标签嵌入js脚本
<link rel="stylesheet" href="...">标签嵌入CSS
<img>展示的图片
<video>和<audio>媒体资源
<object>、 <embed> 、<applet>嵌入的插件
CSS中使用@font-face引入字体
通过<iframe>载入资源

总结:浏览器自己是可以发起跨域请求的(如a标签、img标签、form表单等),但Javascript不能去跨域获取资源(如ajax)

 

2 why

浏览器出于安全考虑(即防止CSRF)会限制跨域读操作,若不加限制则 在一个站点上访问后 本地存储的cookie等信息在访问第二个站点时 就可能泄露了。(从这可见,跨域限制只是在通过浏览器访问时才存在,因此通过HTTP客户端等访问显然没有跨域限制问题)。

没有同源限制时的危害示例

在浏览器上先登录股票网站www.stock.com,得到了cookie,以后再访问stock时浏览器会自动带上cookie;接着访问恶意网站www.beautify.com,假定该网站页面中包含一个恶意js脚本,其行为是去访问stock并把得到的信息发到beautify网站,由于访问stock时浏览器会自动带上cookie故恶意脚本可以成功窃取到数据。示意图如下:

浏览器跨域访问(同源策略)

 

上述过程就是跨站请求伪造(Cross-site request forgery,CSRF)的一种例子,所幸在有浏览器同源策略的限制下上述情况不会发生。

因此,浏览器同源策略的作用是预防跨站请求伪造,也就是用于确保在浏览器内对后端发起访问的前端是该后端信赖的前端

 

后端约束

从上面可看出,为了确保请求者是后端信赖的前端,同源策略是从浏览器请求端来保证的。除了依靠浏览器限制外,也可从后端角度来校验——后端服务可根据所收到请求的origin字段来限制白名单(origin字段由浏览器自动加入HTTP header,用户无法通过编程方式如javascript修改;当然,通过中间人攻击还是可以修改的,此时可用HTTPS或wss防范)。

目前,对于新兴的WebSocket协议(2008年诞生,2011年成为国际标准,目前所有浏览器都已支持),浏览器未做同源限制。因此应用要注意防范CSRF攻击,可借助token或上述的origin等方式防范。

 

3 如何克服浏览器同源限制

浏览器的同源限制是种伤敌一千自损八百的做法,如对于一个大系统来说有很多域名是正常的,同源限制使得同一系统内的不同域名下的服务无法互相访问。

3.1 三种方式

要突破浏览器跨域访问的限制,目前本质上有三种方法:

3.1.1 反向代理

只需要让不同地址对浏览器来说是同源的即可。如可以通过反向代理把需要互相访问的地址放到反向代理后,这样对浏览器来说就是同源的了。参考:通过Nginx反向代理实现跨域访问-cnblogs

 

3.1.2 绕过同源限制JSONP

借助浏览器的跨域资源嵌入来实现跨域资源读取:浏览器对具有src属性的标签(如script、img、iframe等)不做跨域限制,利用这些标签+回到来绕过同源限制实现跨域。这就是所谓的JSONP(JSON with Padding),可以认为是一种取巧方式或非官方的协议。

原理:在页面append一个script标签,标签地址为被跨域访问站点地址,并在地址上加入自定义的回调函数名参数,如?callback=myCallbackFunction,这里的"callback"可以为其他,应事先商定好;被跨域站点的响应逻辑:若未检查到"callback"参数则直接返回data,否则将data作为回调函数名的参数一起返回,即 myCallbackFunction( data );浏览器script加载完后会执行myCallbackFunction函数,因此可以在myCallbackFunction里对请求返回的data进行处理。参考:跨域与跨域访问-csdn

注意区分JSON与JSONP的区别:前者是描述信息的一种格式,后者是信息传递双方约定的一种信息传递方法。

 

3.1.3 协议支持(CORS

W3C标准中的跨域资源共享(Cross-Origin Resource Sharing,CORS)协议。是2014年正式推出的一个标准协议,可认为是浏览器对自身同源策略的妥协或补充,目前绝大多数浏览器都支持。

它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制(在实现上借助了 Access-Control-Allow-Origin 等header来与服务端协商)。在前后端分离的场景下两者独立部署,常借助此来实现前端跨域访问后端。

内部原理(详见:跨域资源共享-阮一峰):简单而言,在向目标服务器发起正式的数据请求前,浏览器先会向其发送 Preflight请求(预请求,为HTTP OPTIONS请求)来询问(或叫协商)其是否允许接下来的跨域请求。“询问”的过程是通过几个请求头和响应头来实现的:

1 浏览器在OPTIONS预请求里增加如下header:

Origin:跨域请求发起者所在的域名
Access-Control-Request-Method:将要发起的跨域数据请求方式(GET/PUT/POST/DELETE/······)
Access-Control-Request-Headers:将要发起的跨域请求中包含的请求头字段

2 目标服务器在响应字段中表明是否允许这个跨域数据请求,浏览器收到后检查如果不符合要求就拒绝后面的数据请求

Access-Control-Allow-Origin:允许哪些域来访问。* 表示允许所有域的请求,然而注意在这里通配符并非万金油,在某些情况(如Cookie跨域共享)下不能用通配符。
Access-Control-Allow-Methods:允许哪些请求方式
Access-Control-Allow-Headers:允许哪些请求头字段
Access-Control-Allow-Credentials:跨域请求时是否允许携带Cookie。
Access-Control-Max-Age:指定本次预检请求的有效期,单位为秒,在此期间对于同样的跨域数据请求,浏览器不用再发预请求询问。

当然,为了避免每次发起数据请求前都要询问,有两个优化措施:

a:一个是上面的Access-Control-Max-Age字段避免每次都要发预请求;

b:另一个是对于“简单请求”浏览器不用发预请求,而是直接在数据请求中带上Origin字段并在响应中检查Access-Control-Allow-Origin,如果不符合要求就报错。“简单请求”是指请求方法为HEAD、GET、POST之一且只包含如下请求头字段的请求(在如今前后端分离的场景下Content-Type通常为json,故几乎都是复杂请求而非简单请求):

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:(application/x-www-form-urlencoded、multipart/form-data、text/plain)

题外话:对于复杂请求,除了上述header字段外,后端还可通过 Access-Control-Expose-Headers 来控制将哪些响应头暴露给前端。不在该header值里的响应头将不会暴露给前端(其实返回了,只是浏览器让其对前端不可见而已,对JavaScript也不可见)。默认情况下有如下6个响应头会暴露给前端而无需特殊指定。详见 CORS规范 的内容。

Content-Type、Content-Language、Cache-Control、Expires、Last-Modified、Pragma

使用

Access-Control-Allow-Origin 的设置(可参阅 Access-Control-Allow-Origin多域名-BAT的乌托邦

基本用法: Access-Control-Allow-Origin: null 、 Access-Control-Allow-Origin: <origin> 、 Access-Control-Allow-Origin: * 。

null 的作用:让data:和file:打开的页面也能够共享跨域资源(因为这种协议下有Origin头,但是值是null,比较特殊)

* 的作用:通配符,允许任意Origin的来源来访问资源。

<origin>:普通的值。需要注意的是浏览器对此值是进行精确的完全匹配,也就是说形如 http://*.baidu.com 里面包含的通配符是不认的,只是当成普通的字面值。

如何设置以允许多个Origin?

1 多个值以逗号分隔行不通,每个值对应的Origin都无法访问到目标资源。

2 设置多个同名header只不过值不同行不通,浏览器只要收到两个Access-Control-Allow-Origin响应头,不论值是什么(即使一模一样),都不会接受。

3 值的域名中使用url pattern表示多个Origin行不通,浏览器拿Access-Control-Allow-Origin的值和Origin进行匹配的规则是完全匹配,通配符只认* 。

4 使用通配符 * :可行,但不安全

5 最佳实践:对于请求者的Origin,服务端进行判断:若允许该跨域则将该Origin赋给Access-Control-Allow-Origin、否则赋为默认值或不返回该响应头。示例:

//java
private Set<String> ALLOW_ORIGINS = new HashList<>();
@Override
public void init() throws ServletException {
    ALLOW_ORIGINS.add("http://localhost:9090");
    ALLOW_ORIGINS.add("http://foo.baidu.com:9090");
    ALLOW_ORIGINS.add("http://bar.baidu.com:9090");
    ALLOW_ORIGINS.add("http://static.yourbatman.cn:9090");
}

private void setCrosHeader(String reqOrigin, HttpServletResponse resp) {
    if (reqOrigin == null) {
        return;
    }
    // 匹配算法:equals
    if (ALLOW_ORIGINS.contains(reqOrigin)) {
        resp.addHeader("Access-Control-Allow-Origin", reqOrigin);
    }
}


//nginx
location / {  
 
 // 枚举列出允许跨域的domian(可以使用NG支持的匹配方式)
 set $cors_origin "";
    if ($http_origin ~* "^http://foo.baidu.com$") {
            set $cors_origin $http_origin;
    }
    if ($http_origin ~* "^http://bar.baidu.com$") {
            set $cors_origin $http_origin;
    }
    add_header Access-Control-Allow-Origin $cors_origin;
}
View Code

相关文章:

  • 2021-12-28
  • 2021-09-09
  • 2021-06-04
  • 2021-12-08
  • 2021-10-29
  • 2021-09-28
  • 2021-11-30
  • 2021-11-18
猜你喜欢
  • 2022-01-19
  • 2021-12-01
  • 2021-12-29
  • 2019-06-21
  • 2021-05-21
  • 2021-06-23
  • 2021-07-31
  • 2021-10-07
相关资源
相似解决方案