HTTP访问控制(CORS)
同源策略
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
为什么不能跨域访问
跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是CSRF跨站攻击原理,无论是否跨域,请求是发送到了后端服务器,注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。所以只有服务器允许跨域,并且在相应包的头信息里面指明允许跨域,那么跨域请求的响应数据就不会被浏览器拦截丢弃了。
什么是跨域
URL1 | URL2 | 是否跨域 | 原因 |
---|---|---|---|
http://kangbiao.org/index | https://kangbiao.org/index | 是 | 协议不同 |
http://kangbiao.org/index | http://kangbiao.org:8080/index | 是 | 端口号不同 |
http://kangbiao.org/index | http://baidu.org/index | 是 | 主机不同 |
http://kangbiao.org/index | http://t1.kangbiao.org/index | 是 | 主机不同 |
通过上面的比较可以归纳出,跨域是指协议、主机地址、端口号这三个条件只要有一个不同则认为是跨域。
六种跨域方式
通过浏览器对象解决
document.domain(适用于子域跨域)
在同源策略中有一个例外,脚本可以设置 document.domain
的值为当前域的一个后缀,如果这样做的话,短的域将作为后续同源检测的依据。例如,假设在 http://t1.kangbiao.org/index 中的一个js脚本执行了下列语句:
document.domain = "kanbgiao.org";
这条语句执行之后,页面将会成功地通过对 http://company.com/index
的同源检测。但是不能通过设置 document.domain = "notkangbiao.org";完成对其他域的访问,该方法只适用于子域和父域之间的跨域解决。
使用document.domain来让子域安全地访问其父域,需要同时将子域和父域的document.domain设置为相同的值,没有这么做的话会导致授权错误。
window.name
这种方案实用性不高,实现也挺麻烦,也不够灵活,所以我就不详细写了,有兴趣可以参考这篇文章
客户端和服务端配合实现jsonp
jsonp其实就是动态创建js脚本。虽然浏览器默认禁止了跨域访问,但并不禁止在页面中引用其他域的JS文件,并可以自由执行引入的JS文件中的函数,因此可以将script的src属性设为需要跨域的接口地址,但是需要服务器将数据组装成js变量定义或者函数传回来,举例如下:
比如kangbiao.org/index 需要调用t1.kangbiao.org/getServerInfo接口获取服务器信息,原来该接口的返回是:
{
"ip":"192.168.1.1",
"cpu":"Intel i5",
"network":"100M"
}
现在为了配合jsonp的话返回格式应该如下:
var response={
"ip":"192.168.1.1",
"cpu":"Intel i5",
"network":"100M"
};
所以jsonp就是在返回数据中定义一个js变量或者函数来实现动态创建js脚本,这样做的缺点也显而易见,会出现变量污染或者函数重名(可以通过生命一个服务器专用的函数对象解决),而且服务器和前端脚本变量绑定太强,不是很灵活。
采用HTML5中的postMessage解决
postMessage可以实现窗口和窗口,页面和iframe,页面和窗口间的跨域通信。
postMessage需要源网站和跨域网站同时实现两个接口postMessage(发送数据)和addEventListener(监听事件,接受数据)
otherWindow.postMessage(message, targetOrigin);
otherWindow
其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message
将要发送到其他 window的数据。
targetOrigin
通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的orign属性完全一致,来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。
target.addEventListener(type, listener[, useCapture]);
type
表示所监听事件类型的一个字符串。
listener
当指定的事件类型发生时被通知到的一个对象。该参数必是实现
EventListener
接口的一个对象或函数。
useCapture
可选
如果值为true, useCapture表示用户希望发起捕获。 在发起捕获之后, 只要Dom子树下发生了该事件类型,都会先被派发到该注册监听器,然后再被派发到Dom子树中的注册监听器中。并且向上冒泡的事件不会触发那些发起捕获的事件监听器。进一步的解释可以查看 DOM Level 3 Events 文档。 请注意该参数并不是在所有的浏览器版本中都是可选的。如果没有指定, useCapture默认为false 。
两个函数的定义如上,addEventListener不是html5中特有的,postMessage是html新增实现跨域通信的。
如果需要跨域交换数据,则需要两边都需要同时实现这两个接口,才能交换数据,不然只能单方向的接收或者发送数据。一般的实现是在addEventListener的回掉函数中通过event.data获取到传过来的数据后,再次调用postMessage将处理后的数据返回给消息来源对象。这样实现好处就是完全不需要后端的参与。但是有一定的安全风险,配合xss可以导致用户凭证信息被盗取。
具体的代码示例参考postMessage实示例
服务器响应头控制(CORS 跨域资源共享)
这种方法是我认为最好的方法,由服务器决定是否允许跨域,如果允许,服务器在响应头中添加相应的字段告诉浏览器此次跨域合法,则浏览器不会将请求包丢弃(文章开头说了跨域其实是浏览器的一种行为),从而完成跨域。
这种方法的详细操作我就不多说了,参考廖雪峰的这篇文章
主要叙述下服务端怎么设置响应头
在PHP中可以中国header()函数设置允许跨域字段
在java中可以通过设置reponse.setHeader()函数来设置,spring4.2及以上版本提供了@CrossOrigin注解来方便实现跨域。
服务器代理
服务器代理就是将需要跨域访问的地址通过服务器访问(服务器此时作为客户端,不会受同源策略限制),然后由服务器返回结果。
例如kangbiao.org/index 页面需要访问api.weibo.com/getNews 来获取最新新闻,我们可以通过在kangbiao.org的服务器上面多增加一个接口 kangbiao.org/api?url=api.weibo.com/getNews ,然后再服务器内部,该接口所做的事情就是向api.weibo.com/getNews 发起请求即可,然后将结果返回。
这样做的好处是实现十分简单,而且可以访问任何跨域站点,缺点就是需要新增维护一个接口,而且如果服务器是通过代理网关,只能内网通信的话也很麻烦。
反向代理
反向代理也是在服务器实现的,主要是通过正则匹配url,匹配成功后重写到目标地址即可,这种方法可以实现所有网站的跨域,不需要服务器提供跨域支持,个人认为比较方便,甚至可以配置kangbiao.org/proxy/weibo/实现将api.weibo.com域的接口整合到我们自己的网站下面来,并且程序不需要做任何改动,改下nginx的配置文件即可。具体的实现方案很多,google或者百度nginx反向代理实现跨域即可。