最近的面试经历让我发现,知其然知其所以然,不能光说出来方法,更要理解它为什么要这么做 。梳理下为什么会出现同源策略,同时引出相应的跨域方法。
Same-Origin Policy on its own increases security but is not enough to prevent all Cross-Site Request Forgery (CSRF) attacks, which basically are an attempt to take advantage of different origins. That is why anti-CSRF tokens should still be used as an additional form of protection. SOP is also completely useless as a method of protection against Cross-site Scripting (XSS) because it would have to limit loading of scripts from different sites and that would completely hinder the functionality of web applications. www.acunetix.com/blog/web-se…
上面简单来说同源策略本身是增加了安全性,但是不能防止所有Cross-Site Request Forgery (CSRF)和Cross-site Scripting (XSS),这些攻击基本上是企图利用不同源。所以它相当于浏览器基本的保护,但是限制更多会导致开发困难,所以合理的准守策略,不应该什么都跨域。
同源策略(Same-origin Policy )最早是Netscape 工程师解决加载资源管理的关边界关系,出现的规则。满足协议、 URL 和端口一致。如果在没有同源策略的情况,你被诱导进入假的银行网站,但是里面用iframe加载真的银行网站,你进入之后所以信息都会被假的网站获取iframe里面的窃取信息。
同源策略其实不光是针对请求API,操作iframe里的docment等,其实Web上所以的资源都是应用这种检查机制。
同源策略只会出现在浏览器下,有时候发送请求但是控制台显示失败,不是没法送出去,是浏览器拦截你的响应,服务器这个时候已经接到的你的请求。
源的继承
如果打开一个新的窗口about:blank和javascript:URL 执行脚本,用这个窗口执行脚本也会使用创建它的父级源。
let blankTab = window.open('about:blank')
//在新打开的about:blank
//document.domain等于父级源
源的更改
但是只能修改到父级域名
//store.company.com => company.com
document.domain = "company.com";
//以下会触发异常
//1.在iframe元素里document 因为源不同,所以不能设置iframe里面的document到父级域名
//2.document没有browsing context
//3.document.domain 为null
//4.document.domain 不为有效值
//5.响应头 Feature-Policy:enabled
跨域访问可以分为三种情况
- Cross-origin writes 和Cross-origin embedding通常是允许的,Cross-origin writes 跳转,重定向连接跳到另一个网站。Cross-origin embedding 加载CSS,JavaScript,多媒体等资源。
- Cross-origin reads 通常是不允许的,就像你用canvas在里面读取跨域图片会报跨域问题(例如img标签可以加载图片,属于直接嵌入不能读取和写入图片的信息)。
| 类型 | 说明 |
|---|---|
| iframe | 跨域的话需响应头配置X-Frame-Options,同时跨域不能访问iframe里面的docment |
| CSS | 允许 @import加载跨域资源 |
| image | 允许嵌入,读取受限 |
| multimedia | 允许嵌入 |
| script | 允许嵌入,但是会拦截跨域请求的api |
JSONP就是利用script标签可以加载
//http://www.example.com/jsonp?callback=foo
//返回数据 foo([1,2,3,4]);
function jsonp(url, jsonp, callback) {
//创建触发script标签
let script = document.createElement('script')
script.src = url
script.type = 'text/javascript'
document.body.appendChild(script)
//定义获取数据接收后的函数
window[jsonp] = (data)=> {
callback(data)
}
document.body.appendChild(script)
}
jsonp('http://www.example.com/jsonp', 'foo',(data)=>console.log(data))
跨源脚本可以访问的API
其实大部分的属性只读,方法都是功能性的例如关闭,跳转等,window.postMessage 属于双方约定的形式,暴露出来的操作相对安全。
| 方法 | 说明 |
|---|---|
| window.close | 正常 |
| window.focus | 正常 |
| window.postMessage | 正常 |
| location.replace | 正常 |
| window.blur | 正常 |
| 属性 | 读写权限 |
|---|---|
| window.closed | Read only |
| window.frames | Read only |
| window.length | Read only |
| window.location | Read/Write |
| window.opener | Read only |
| window.self | Read only |
| window.parent | Read only |
| window.top | Read only |
| window.window | Read only |
Cross-Origin Resource Sharing (CORS)
其实可以看出要是遵循浏览器的跨域规则也能使用的,再不行也有JSONP。但是JSONP是单向只读的,而随着Web的发展满足不了现在的需求,需要一个又能宽松和安全的策略,浏览器厂商们就制订解决方案,但这个时候微软在IE9和IE8中XDomainRequest是它的解决方案。但Chrome, Firefox和一些其他厂商实现的就是CORS,随着Chrome的崛起,微软只能在IE10上兼容CORS和XDomainRequest。
其实简单说的话,就是约定服务器给的响应头来判断跨域请求是否允许,例如常见的Access-Control-Allow-Origin: www.example.com 就是允许来着网站请求www.example.com。
详细说的话其实主要分两种情况简单的请求(Simple Request)和预检请求(Preflight Request)。这样就可以解释为什么我们用POST和GET请求也会触发option探测请求了,因为现在有时候开发验证用JWT需要约定Authorization: Bearer <token> 请求头,或者跟服务器协商约定的请求头,都会触发预检请求(Preflight Request)。
这也能解释为什么websocket能跨域因为cors和sop没有约束它
- 完成 websocket 不需要 HTTP 响应数据,
- 数据传输通过 WebSocket 协议进行。
想了解更多可以看看这篇文章
blog.securityevaluators.com/websockets-…
下面就是二种情况,大家也可以结合项目对应看看满足了哪些条件,触发了什么请求也能更好地理解。
简单请求(Simple Request)
- HTTP方法:
GETHEADPOST
- 不能设置集合之外的其他首部字段:
AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth
Content-Type的值仅限于下列三者之一:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
预检请求(Preflight Request)
- 使用了下面任一 HTTP 方法:
PUTDELETECONNECTOPTIONSTRACEPATCH
- 设置集合之外的其他首部字段:
AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth
Content-Type的值不为下列三者之一:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
HTTP响应头
这些需要服务端配置的响应头
| 响应头 | 例子 |
|---|---|
| Access-Control-Allow-Origin | Access-Control-Allow-Origin: http://www.example.com |
| Access-Control-Expose-Headers | Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header |
| Access-Control-Max-Age | Access-Control-Max-Age: 600 |
| Access-Control-Allow-Credentials | Access-Control-Allow-Credentials: true |
| Access-Control-Allow-Methods | Access-Control-Allow-Methods: POST, GET, OPTIONS |
| Access-Control-Allow-Headers | Access-Control-Allow-Headers: X-Custom-Header |
如果需要带Cookie的时候
// 服务端响应头需要添加 Access-Control-Allow-Credentials: true
// 这样服务器和客户端就可以互相发送Cookie了
// 需要注意服务端响应头Access-Control-Allow-Origin:“*” 的时候请求会失败,应该设置相应的域名
// 例:Access-Control-Allow-Origin:“127.0.0.1”
// 客户端代码
let xhr = new XMLHttpRequest();
let url = 'http://127.0.0.1/api/user';
xhr.open('get',url,true)
xhr.withCredentials = true;
xhr.send();
反向代理
其实本身代理跟主题没什么关系,但为了完备性吧,因为我们要通过代理目的是为了做到同源,通过在一个和你同源的服务器上,把跨域请求的响应都通过代理服务器接收这样就是同源的了。正向代理是服务于客户端的,相当于让服务端感知不到客户端例如大家用的SSR。反向代理服务于服务端的,隐藏服务端例如负载均衡用户不知道他访问的是最后是什么服务器。因为我们要屏蔽服务端这里选用反向代理。
这里借助http-proxy-middleware,就是webpack-dev-server里的proxy。这样请求/api的时候相当于请求http://otherDomain:1234/api
let express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:1234/api', changeOrigin: false }));
module.exports = app
总结
其实把同源策略理清,就能看见很多大家用的跨域方法,这样能更好的记忆,而且遇到问题能够更好的应对。我建议开两台本地的服务测试下,能够更好的理解。
第一次写博文,有什么错误请指正,对我来说也是一个交流的机会,谢谢大家~
参考链接