一、概述
1.1 Web页面安全
说到Web页面安全你会想到哪些问题呢?
- 什么是同源、什么是同源策略、同源策略的表现是什么?
- 浏览器出让了同源策略的哪些安全性?
- 跨域解决方案有哪些?
- JSONP如何实现?
- CORS你了解哪些?
- 针对Web页面安全有哪些常见的攻击手段(XSS、CSRF)?
1.2 浏览器网络安全
说到浏览器网络安全你会想到哪些问题呢?
- 什么是请求报文?
- 什么是响应报文?
- HTTP缺点
- HTTPS基础知识点
- HTTPS流程
1.3 浏览器系统安全
说到浏览器系统安全你会想到哪些问题呢?
- 安全沙箱
二、Web页面安全
2.1 同源策略
2.1.1 同源
跨域本质其实就是指两个地址不同源,不同源的反面不就是同源,同源指的是:如果两个URL的协议、域名和端口号都相同,则就是两个同源的URL。
// 非同源:协议不同 http://www.baidu.com https://www.baidu.com // 同源:协议、域名、端口号都相同 http://www.baidu.com http://www.baidu.com?query=1
2.1.2 同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的加载的脚本如何能与另一个源的资源进行交互。其主要是为了保护用户信息的安全,防止恶意的网站窃取数据,是浏览器在Web页面层面做的安全保护。
2.1.3 同源策略的表现
既然同源策略是浏览器在Web页面层面做的保护,那么该层面哪些位置需要进行保护呢?总结下来主要包含三个层面:DOM层面、数据层面、网络层面。
- DOM层面
同源策略限制了来自不同源的JavaScript脚本对当前DOM对象读和写的操作。
- 数据层面
同源策略限制了不同源的站点读取当前站点的Cookie、IndexedDB、localStorage等数据。
- 网络层面
同源策略限制了通过XMHttpRequest等方式将站点的数据发送给不同源的站点。
2.1.4 浏览器出让了同源策略的哪些安全性?
2.2 跨域分类
同源策略保证了浏览器的安全,但是如果将这三个层面限制的死死的,则会让程序员的开发工作举步维艰,所以浏览器需要在最严格的同源策略限制下做一些让步,这些让步更多了是在安全性与便捷性的权衡。其实跨域的方式就可以认为是浏览器出让了一些安全性或在遵守浏览器同源策略前提下所采取的一种折中手段。
2.2.1 DOM层面和数据层面分类
根据同源策略,如果两个页面不同源,无法互相操作DOM、访问数据,但是两个不同源页面之间进行通信是比较常见的情形,典型的例子就是iframe窗口与父窗口之间的通信。随着历史的车轮,实现DOM层面间通信的方式有多种,如下所示:
- 片段标识符
片段标识符其核心原理就是通过监听url中hash的改变来实现数据的传递,想法真的很巧妙。
// 父页面parentHtml.html <!DOCTYPE html> <html lang="zh"> <head> <title></title> </head> <body> 我是父页面 <button id='btn'>父传给子</button> <iframe src="./childHtml.html" id="childHtmlId"></iframe> </body> <script> window.onhashchange = function() { console.log(decodeURIComponent(window.location.hash)); }; document.getElementById('btn').addEventListener('click', () => { const iframeDom = document.getElementById('childHtmlId'); iframeDom.src += '#父传给子'; }); </script> </html>
// 子页面childHtml.html <!DOCTYPE html> <html lang="zh"> <head> <title></title> </head> <body> 我是子页面 <button id='btn'>子传给父</button> </body> <script> window.onhashchange = function() { console.log(decodeURIComponent(window.location.hash)); }; document.getElementById('btn').addEventListener('click', () => { parent.location.href += '#子传给父'; }); </script> </html>
- window.name
浏览器窗口有window.name属性,这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。如果需要实现父页面和跨域的子页面之间的通信,需要一个和父页面同源的子页面作为中介,将跨域的子页面中的信息传递过来。(好麻烦呀,强烈不推荐使用,此处就不写对应的代码啦)
- document.domain
document.domain是存放文档的服务器的主机名,可通过手动设置将其设置成当前域名或者上级的域名,当具有相同document.domain的页面就相当于处于同域名的服务器上,如果其域名和端口号相同就可以实现跨域访问数据了。
- postMessage(强烈推荐)
window.postMessage是HTML5新增的跨文档通信API,该API,允许跨窗口通信,不论这两个窗口是否同源。
// 父页面 <!DOCTYPE html> <html lang="zh"> <head> <title></title> </head> <body> 我是父页面 <button id='btn'>父传给子</button> <iframe src="http://127.0.0.1:5500/024/childHtml.html" id="childHtmlId"></iframe> </body> <script> window.addEventListener('message', function(event) { console.log('父页面接收到信息', event.data); }); document.getElementById('btn').addEventListener('click', () => { const iframeDom = document.getElementById('childHtmlId'); iframeDom.contentWindow.postMessage('我是执鸢者1', 'http://127.0.0.1:5500/024/childHtml1.html'); }); </script> </html>
// 子页面 <!DOCTYPE html> <html lang="zh"> <head> <title></title> </head> <body> 我是子页面 <button id='btn'>子传给父</button> </body> <script> window.addEventListener('message', function(event) { console.log('子页面接收到信息', event.data); }); document.getElementById('btn').addEventListener('click', () => { parent.postMessage('我是执鸢者2', 'http://127.0.0.1:5500/024/parentHtml1.html'); }); </script> </html>
2.2.2 网络层面
根据同源策略,浏览器默认是不允许XMLHttpRequest对象访问非同一站点的资源的,这会大大制约生产力,所以需要破解这种限制,实现跨域访问资源。目前广泛采用的主要有三种方式(注:该出不给出具体代码,后续会有专门的百题斩进行详细阐述):
2.2.2.1 通过代理实现
同源策略是浏览器为了安全制定的策略,所以服务端不会存在这样的限制,这样我们就可以将请求打到同源的服务器上,然后经由同源服务器代理至最终需要的服务器,从而实现跨域请求的目的。例如可以通过Nginx、Node中间件等。
2.2.2.2 JSONP的方式
JSONP是一种借助script元素实现跨域的技术,它并没有使用XMLHttpRequest对象,其能够实现跨域主要得益于script有两个特点:
(1)src属性能够访问任何URL资源,并不会受到同源策略的限制;
(2)如果访问的资源包含JavaScript代码,其会在下载后自动执行。
下面一起来实现一下JSONP
- 全局挂载一个接收数据的函数;
- 创建一个script标签,并在其标签的onload和onerror事件上挂载对应处理函数;
- 将script标签挂载到页面中,向服务端发起请求;
- 服务端接收传递过来的参数,然后将回调函数和数据以调用的形式输出;
- 当script元素接收到影响中的脚本代码后,就会自动执行它们。
function createScript(url, charset) { const script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); charset && script.setAttribute('charset', charset); script.setAttribute('src', url); script.async = true; return script; } function jsonp(url, onsuccess, onerror, charset) { const hash = Math.random().toString().slice(2); window['jsonp' + hash] = function (data) { if (onsuccess && typeof(onsuccess) === 'function') { onsuccess(data); } } const script = createScript(url + '?callback=jsonp' + hash, charset); // 监听加载成功的事件,获取数据,这个位置用了两个事件onload和onreadystatechange是为了兼容IE,因为IE9之前不支持onload事件,只支持onreadystatechange事件 script.onload = script.onreadystatechange = function() { //若不存在readyState事件则证明不是IE浏览器,可以直接执行,若是的话,必须等到状态变为loaded或complete才可以执行 if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') { script.onload = script.onreadystatechange = null; // 移除该script的DOM对象 if (script.parentNode) { script.parentNode.removeChild(script); } // 删除函数或变量 window['jsonp' + hash] = null; } }; script.onerror = function() { if (onerror && typeof(onerror) === 'function') { onerror(); } } // 添加标签,发送请求 document.getElementsByTagName('head')[0].appendChild(script); }
2.2.2.3 CORS方式
跨域资源共享(CORS),该机制可以进行跨域访问控制,从而使跨域数据传输得以安全进行。(实现一个跨域请求的方式,其中html访问网址为http://127.0.0.1:8009; 服务器监听端口为:8010) 一、整体流程
CORS的通信流程是浏览器自动完成,不需要用户参与,其核心点是服务器,只要服务器实现了CORS接口就可以实现跨源通信了。虽然是浏览器自动完成,但是浏览器其实还是根据请求时字段的不同分为简单请求和非简单请求的,下面对这两者进行简要介绍。
- 简单请求 (1) 定义
只要满足以下两个条件就属于简单请求:
- 请求方法是一下三种方法之一:HEAD、GET、POST;
- HTTP的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(其值为application/x-www-form-urlencoded、multipart/form-data、text/plain三个中的一个)。
(2)流程
简单请求的整个流程可以归结为以下步骤:
1) 浏览器直接发出CORS请求,具体来说就是在头信息之中增加一个Origin字段,该字段用来说明请求来自哪个源(协议+域名+端口),服务器根据这个值决定是否同意这次请求;2)当服务器接收到请求后,根据Origin判定指定的源是否在许可的范围内。3)如果不在许可范围内,服务器会返回一个正常的HTTP回应,浏览器发现该回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出错误,被XML的onerror回调函数捕获。(注意:由于正常回应,其状态码为200,所以该错误不能通过状态码识别) 4)如果Origin指定的域名在许可范围内,服务器返回的响应中会多出几个头信息字段(Access-Control-Allow-Origin、Access-Control-Allow-Credentials、Access-Control-Expose-Header等)。
(3)关键字段
1) Access-Control-Allow-Origin
必须字段,该值要么是请求的Origin字段值,要么是一个*(表示接受任意域名的请求)。
2)Access-Control-Allow-Credentials
可选字段,其值是一个布尔值,表示是否允许发送Cookie。默认是不发送Cookie值,当设置为true时,表示服务器明确许可,Cookie可以包含在请求中发送给服务器。(注意:发送Cookie时要注意两点:一方面在Ajax请求中需要设置withCredentials属性;另一方面不能将Access-Control-Allow-Origin设置为*,需要指定明确的、与请求网页一致的域名)
3)Access-Control-Expose-Header
可选字段,当CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段(Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma),如果想获取其它字段必须在Access-Control-Expose-Header中指定。
- 非简单请求
(1)定义
不是简单请求的就是非简单请求,非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或Delete,或者Content-Type字段的类型是application/json.
(2)流程
非简单请求相比于简单请求较复杂,在发起正式请求之前会进行一次预检请求,通过预检请求的结果来决定是否进行后续的正式通信。
1)浏览器发起预检请求,该请求的请求方法是options,该请求是用来询问的;2)服务器收到“预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。3)如果浏览器否定了“预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段,这时浏览器就会认定服务器不同意预检请求,触发错误;4)如果浏览器通过了“预检”请求,以后每次浏览器正常的CORS请求就跟简单请求一样,会有一个Origin头信息字段,服务器的回应也会有一个Access-Control-Allow-Origin头信息字段;
(3)关键字段
1)Access-Control-Request-Method
必须字段,用来列出浏览器的CORS请求会用到哪些HTTP方法。
2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,用来指定浏览器CORS请求会额外发送的头信息字段。
3)Access-Control-Allow-Methods
必须字段,该值是一个逗号分隔的字符串,用来表明服务器支持的所有跨域请求的方法。
4)Access-Control-Allow-Headers
该值是一个逗号分隔的字符串,表明服务器支持的所有头信息字段。
5)Access-Control-Max-Age
用来请求预检请求的有效期,单位为秒。
- 简单实现
(1)html页面内容
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>test CORS</title> </head> <body> CORS <script src="https://code.bdstatic.com/npm/axios@0.20.0/dist/axios.min.js"></script> <script> axios('http://127.0.0.1:8010', { method: 'get' }).then(console.log) </script> </body> </html>
(2)服务器端代码
const express = require('express'); const app = express(); app.get('/', (req, res) => { console.log('get请求收到了!!!'); res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8009'); res.send('get请求已经被处理'); }) app.listen(8010, () => { console.log('8010 is listening') });