跨域是指因为浏览器因为同源策略对两个不同域(网站)之间进行交互限制的一种表现. 跨域不是请求没有发出或者响应,浏览器发出了请求,服务器接收请求后正常响应,但是因为浏览器的同源策略(此处浏览器背锅)对返回的响应进行拦截,返回给我们跨域的报错.
浏览器的同源策略
同源策略是一个重要的安全策略,它是浏览器最核心也是最基本的安全功能,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互.它能帮助阻隔恶意文档,减少可能被攻击的媒介.
同源
根据RFC-1738
而实际在浏览器上,URL通常是有以下组成
// protocol: 协议
// host: 主机
// domain: 域名
// port: 端口
// path: 资源路径/请求路径
// query: 参数(get)
protocol://host.domain:port/path?query
如果两个URL的 协议、主机+域名(通常将这两个说为域名)、端口,都相同的话,那么这两个URL同源,这个方案也被称为“协议/主机/端口元组” 而IE则在同源策略的有以下两个差异点:
- 授信范围: 两个相互之间高度互信的域名不受同源策略的限制
- 端口: IE未将端口纳入同源策略的检查,即 协议、主机+域名 一致就是同源.
URL | 表现 | 原因 | www.test.com/page1 www.test.com/page2 | 同源 | 路径不同 | www.test.com/page1 www.test.com/page1 | 不同源 | 协议不同 | www.test.com/page1 test.test.com/page1 | 不同源 | 主机不同 | www.test.com/page1 www.qq.com/page1 | 不同源 | 域名不同 | www.test.com/page1 www.test.com:8080/page1 | 不同源 | 端口不同 | http://localhost/page1 http://127.0.0.1/page1 | 不同源 | 主机不同 |
---|
源的设置
在页面通过about:blank
(打开浏览器空白页)或者javascript:URL
(伪协议)执行的脚本会继承打开该URL的文档源,因为这些类型的URL没有包含源服务器的相关信息.
某些情况下,可以修改页面的源:
- 可以通过脚本设置
document.domain
为当前域或当前域的父域,如果设置为当前域的父域,则这个父域用于后续源的检查 - 任何对
document.domain
的赋值操作都会导致端口号被重写为null
通过设置document.domain
使子域与父域通信时,必须使父域与子域设置为相同的值(域名 + 端口).
跨域网络访问
- 跨域写操作(cross-origin writes):一般是被允许的,如:links、重定向、表单提交
- 跨域资源嵌入(cross-origin embedding):一般是被允许的,如:script、img、video、iframe
- 跨域读操作(cross-origin reads):一般是不被允许的,如:DOM、JS对象、Cookie、LocalStorage、IndexDB、ajax请求
阻止跨源访问
- 阻止跨域写操作,只要检测请求中一个不可推测的标记(CSRF token)即可,这个标记被称为Cross-Site Request Forgery标记
- 阻止资源的跨域读取,需要保证该资源是不可嵌入的.阻止嵌入行为是必须的因为嵌入资源通常向其暴露信息
- 阻止跨站嵌入,保证你的资源不是上述可允许的嵌入资源格式
那么如何解决跨域问题呢
window.name
在一个窗口的生命周期内,窗口载入的所有页面都共享一个window.name
,每一个页面对window.name
都有读写的权限,window.name
可以持久存在一个窗口载入过的所有页面中,并且可以存储2M的字符串.
降域
降域通过设置document.domain
来实现跨域,但是只适用于二级域名相同的域,用来实现不同子域的框架之间的交互.
参考上面的源的设置上.
JSONP
JSON with Padding,因为同源策略的影响,ajax请求不同域上的数据获取不到(cross-origin reads),但是(cross-origin embedding)跨域资源嵌入仍然是可以的,因此我们可以通过调用跨域服务器上动态生成的js格式文件,来获取跨域服务器上的数据,但是这样只是下载了数据,如何把数据提供到我们的内存中去调用呢,这就需要去定义一个回调函数,服务器动态生成的js文件中包含callback(响应)
,这样当js加载完成后,执行callback把跨域的数据加入到内存中. 其工作原理就是:客户端定义一个回调函数,服务端通过调用这个函数并把响应作为参数,完成回调获取跨域数据.
# html
<script>
function callAlert(data){
alert(data,'-------data')
}
const url = 'http://www.test.com/search?callback=callAlert'
const script = document.createElement('script')
script.src = url
document.body.append(script)
</script>
# php
exit($_GET['callback'].'({errNo:0,data:"success"})')
- 优点:JSONP 兼容性好
- 缺点: 只支持GET(本质上是通过资源嵌入实现,根XMLHttpRequest无关); 只能解决跨域请求,不能解决不同域页面或者iframe的通信问题;JSONP从其他域动态生成JS,可能会携带恶意代码;如果遇到服务端的错误无法处理(XMLHttpRequest相对于JSONP有更好的错误处理机制)
空的iframe + form表单
JSONP只能实现GET请求的跨域,那么POST跨域请求呢,我们可以通过空的iframe + form表单,利用form表单的提交不受同源策略限制来实现跨域.
const iframe = document.createElement('iframe')
iframe.name = 'test'
iframe.style.display = 'none'
document.body.appendChild(iframe)
// 提交成功,回调处理
iframe.onLoad = () => {
console.log('success')
}
// form 跨域操作
const form = document.createElement('form')
const input = document.createElement('input')
input.name = 'data'
input.value = 'xxx'
form.appendChild(input)
// 提交后跳转至iframe
form.target = 'test'
form.action = url
form.method = 'post'
form.style.display='none'
document.body.appendChild(form)
form.submit()
// 跨域完成,移除form
document.body.removeChild(form)
CORS(Cross-Origin Resource Share)
跨域资源共享,是一种基于http头的机制(新增一组HTTP首部字段),允许服务器声明哪些源站通过浏览器可以访问哪些资源,使浏览器可以跨域网络访问,跨域数据传输得以安全进行,克服同源策略的限制;还通过一种机制检查服务器是否会允许要发送的真实的请求,对那些可能对服务器数据产生副作用的http请求方法,浏览器必须首先使用OPTIONS
方法发起一个预检请求(preflight request),从而获知服务器是否允许该跨域请求,确认允许后,才发起实际的http请求,在预检请求的返回中服务器也可以通知客户端是否需要携带身份凭证.
- 需要服务器和浏览器同时支持,整个cors通信过程中,都是浏览器自动完成,不需要用户参与,浏览器一旦发现ajax请求跨域,就会自动添加一些附加头信息,因此关键在于服务器,需要服务器处理新的请求头和响应头实现cors接口.
- cors请求失败会产生错误,但是为了安全,在JS代码层面无法获知哪里出错,只能通过浏览器的控制台查看
- cors支持的场景:XMLHttpRequest或Fetch发起的跨域http请求;web字体;webGL贴图;使用 drawImag 将Images/video 画面绘制到canvas
浏览器将cors请求分为两种: 简单请求和复杂请求
- 简单请求(不会触发cors预检请求)
- 请求方法为以下之一: GET、POST、HEAD
- 除了被用户代理自动设置的首部字段和在Fetch规范中定义为禁用首部名称的其他首部,http头部包含:Accept、Accept-Language、Content-Language、Content-Type、(DPR、Downlink、Save-Data、Viewport-Width、Width ---- 未知)
- Content-Type为以下之一:text/plain、multipart/form-data、application/x-www-form-urlencoded
- 请求中没有使用
ReadableStream
对象(未知) - 请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器,XMLHttpRequestUpload
可以通过XMLHttpRequest.upload
访问(未知)
// 请求头
GET /resource/public-data/ HTTP/1.1
HOST:bar.other
...
Origin:http://foo.example // 表明请求源
// 响应头
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: * // 表明该资源可以被任意外域访问,该字段表示接受的源
Access-Control-Allow-Credentials: true // 表示允许cors访问携带cookie
Access-Control-Expose-Headers:Cache-Control // cors访问可获取的响应头
Content-Type: application/xml
- 复杂请求: 非简单请求均为复杂请求,会触发cors预检请求
- 预检请求: 浏览器使用
OPTIONS
方法发起一个请求到服务器,以获知服务器是否允许该实际请求,可以避免跨域请求对服务器的用户数据产生未预期的影响.
- 预检请求: 浏览器使用
// 预检请求
// 请求头
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
...
origin: http://foo.example
Access-Control-Request-Method: POST // 告知服务器,实际请求采用POST方法
Access-Control-Request-Headers: X-PINGOTHER, Content-Type // 告知服务器,实际请求,携带两个自定义首部字段,X-PINGOTHER和Content-Type
// 响应头
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: http://foo.example // 服务器允许的源
Access-Control-Allow-Methods: POST, GET, OPTIONS // 服务器允许的方法
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 服务器允许的自定义请求头
Access-Control-Max-Age: 86400 // 预检请求的有效期,有效期内不需要为同一个请求发出预检请求
// 实际请求
// 请求头
POST /resources/post-here/ HTTP/1.1
Host:bar.other
...
origin: http://foo.example
X-PINGOTHER:pingpong
Content-Type:text/xml;charset=UTF-8
// 响应头
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: http://foo.example
...
需要注意的是:
- 大多数浏览器不支持预检请求的重定向,只能通过服务器去掉预检请求重定向或者把实际请求变为一个简单请求
- XMLHttpRequest或Fetch可以基于Cookies和http认证信息发起携带身份凭证的cors访问,但是需要在脚本中为
XMLHttpReuest.withCredentials
设置为true,否则浏览器不会自动携带,同时服务端需要在响应头设置Access-Control-Allow-Credentials:true
,否则浏览器不会把响应返回 - 对于携带身份凭证的请求,服务器不得设置
Access-Control-Allow-Origin:*
,否则将会请求失败,必须设置为具体的域
postMessage(html5新特性)
目前IE8、Chrome、Firefox、Opera等浏览器均已支持,可以向其他window对象发送消息(无论这个window对象是否同源)
- 页面和其打开的窗口的数据传递
- 多窗口之间的数据通信
- 页面与嵌套的iframe消息传递
// otherWindow:其他窗口对象的引用
// message: 传递的数据
// target:要发送的窗口或地址,可以是字符串“*”或者是一个URI
// transfer: 是一串和message同时传递的Transferable对象,这些对象的所有权将转移给消息的接收方,发送方不再拥有
otherWindow.postMessage(message,target,[transfer])
// 接受消息
windiw.addEventListener('message',({source,origin,data}) => {
// source: 发送消息的窗口
// origin: 消息的目的网址
// data: 消息
...
})
WebSocket
区别与HTTP协议只能客户端发起请求,服务端响应,WebSocket可以实现客户端与服务端的双向通讯
- 建立在tcp协议上,服务端实现容易
- 与http协议有着良好兼容性,默认端口80和443,握手阶段采用http协议,不容易屏蔽,能通过各种http代理服务器
- 数据格式轻量,性能开销小,客户端与服务端进行数据交换时,服务端到客户端的数据包头只有2-10个字节,客户端到服务端需要再加上4个字节的掩码,而Http协议每次都需要携带完整头部
- 更好的二进制支持,可以发送文本和二进制数据
- 没有同源限制
- 协议标示符ws (加密:wss),请求地址就是后端支持websocket的API
客户端发起Http握手,告知服务端采用websocket协议通信 + websocket协议版本,服务端确认协议版本,升级为websocket协议.升级完成后,后续数据交换遵循websocket协议.
// 握手阶段
// 请求头
connection: upgrade // 需要升级协议
upgrade: websocket // 升级为websocket协议
sec-WebSocket-Version: 13 // 升级的协议版本,如果服务端不支持,返回的响应头包含 Sec-WebSocket-Versionheader,包含服务端支持的版本号
Sec-WebSocket-Key: bwb9SFiJONXhQ/A4pLaXIg== // 对应服务端响应头的Sec-WebSocket-Accept,由于没有同源限制,websocket客户端可以任意连接支持websocket服务,这个字段相当于标示,避免多余无用的连接
...
// 响应头
...
connection: upgrade // 升级协议
upgrade: websocket // 升级为websocket协议
Sec-WebSocket-Accept: 2jrbCWSCPlzPtxarlGTp4Y8XD20= // 告知客户端服务器同意发起websocket连接,值根据请求头Sec-WebSocket-Key计算.
WebSocket API
- 创建连接
const ws = new WebSocket('wss://echo.websocket.org')
- websocket.readyState: websocket连接当前状态
- CONNECTING = 0 ,正在建立连接;
- OPEN = 1,连接成功,可以通信;
- ClOSING = 2, 正在关闭;
- CLOSED = 3,连接关闭或打开连接失败
- 连接建立成功回调
ws.onopen = () => {...}
- 连接建立失败回调
ws.onclose = () => {...}
- 接收到服务端数据
ws.onmessage = ({data}) => {...}
- 向服务器发送数据
ws.send(data)
- 还有多少字节的二进制数据没有发送出去,判断发送是否结束,
ws.bufferedAmount
- 报错回调
ws.onerror = (evt) => {...}
nginx|Node|Apache 代理服务器(反向代理)跨域
CSRF(Cross-Site Request Forgery)
跨站请求伪造,通过伪装成受信任的用户,向服务端发起请求.
防御:
- referer: 因为伪造的请求一般是从第三方网站发起的,所以只要判断请求头中的referer不是本站,就判断为csrf(只能防御跨站csrf,不能防御本站csrf)
- 验证码: 第三方网站无法获取验证码
- token: 每一个网页包含服务器产生的token,提交时将token传递至服务器,由服务器进行判断
- post: 相比于get请求,post请求的更加安全.
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!