本章节的内容注重实际应用,通过讲解在用户从浏览器地址栏输入网址到浏览器渲染出页面的过程中发生了什么来穿插学习内容,达到学习以后可以掌握在开发中熟练使用 HOST 代理和抓包分析的目的。
我们来用一个图看看一个最普通的请求是怎么发生的。
由于我们的互联网是基于TCP/IP来构建的,所以浏览器在输入域名之后,其实浏览器并不知道你想访问的服务器到底是网络上的哪个设备,它必须要去找到这个域名对应的IP地址,然后把请求发送到这个IP地址上。
如图:浏览器查询域名与IP地址关系的服务器叫做DNS服务器,该服务器没有域名,直接以IP地址的形式保存在用户计算机的配置里面。
就这样吗?那原来还没有发明DNS服务器的时候,人们是怎么访问互联网的?
最开始,我们还会使用域名来直接访问一个服务器,如图:
这个时候,还要自己在脑子里面记住很多IP地址,由于IP地址是由点分十进制的数字组成的,对于人类来说并不是特别好记忆,于是有人就想,我们是不是可以用英文字符串来代指某个网站或者资源呢?
于是,人们维护一个叫做HOST的文件,在这个文件中,记录了当时为数不多的域名和它们对应IP的关系。(那么肯定有一些IP没有域名,那我们怎么访问它呢?我们可以通过在浏览器中直接输入IP地址的方式访问它们)
但是随着拥有域名的站点越来越多,大家的HOST文件都无法保证 1)同步更新,2)收录最新最完整的列表。这个时候有这么一种想法,能不能把这些HOST放到固定IP上,大家都去这个固定IP请求该文件呢?甚至,我们可不可以不请求整个庞大的文件,而是只让它告诉自己想要访问那个域名的IP就行了呢?这个就是我们现在DNS服务器的原理:我们让它告诉我们需要访问的域名的IP地址。我们就这样成功的把本地冗长的HOST文件移动到了一个公共服务器上。
虽然每个电脑本地的HOST文件的内容都没有了,但是这个机制还是留存着:操作系统会先去本地HOST查询域名,如果查到了,直接访问;如果没有查到,再去远程DNS查询。
理解了以上的内容我们来看看我们要学会的第一个技能:作为一个开发者,我们假如仍然想用真实的域名welkin.online
去访问我们的开发机器上面的服务器10.10.10.1
,那我们应该如何做呢?或者我们需要把一个开发用的域名如 dev.welkin.online
绑定到自己开发用的机器10.10.10.1
上呢?有多种方式:1)我们可以修改想要用来访问的计算机的HOST文件,让该域名指向开发机器;2)我们可以自己搭建DNS服务器,让想要访问的计算机的DNS设置为这个DNS服务,然后在DNS服务中将开发机器的IP与想要访问的域名进行绑定。
现在我们开始了手机页面的开发,这个时候我们想要用手机真机来调试,我们却发现,在现在的iOS手机上,我们是没有权限访问到HOST文件的。那我们只剩下自建一个DNS服务器了?其实并不是的,接下来我们来学习一种新的方式-代理
代理就是用户A把所有的请求都通过一个外界的服务器B来完成对一些网站C的请求。从被请求的网站C来看,所有的请求就是来自B的IP,而不是A。那么这个和我们将手机的请求定位到开发机器上有什么关系呢?我们可以这么看看。在B服务器的HOST中将开发机器和相应域名绑定,这样被代理过来的流量走到B机器的时候,也会查询这个HOST,这样就能达成手机访问开发机器了。
PS:这个代理过程也可以全部发生在手机上,即AB都是我们的手机。有一些调试和代理iOS app,可以将手机所有的流量接管并且代理到指定的机器上,也可以在该APP内部将流量分流,将dev.welkin.online
直接指向开发机器10.10.10.1
如图:
- DNS服务:
- HOST文件:
1 在各大操作系统上,HOST文件都在什么目录?该文件的用户组权限是什么?
2 DNS协议的默认端口是什么?如何自己架设一个DNS服务器
3 画出代理模式的网络请求图
我们来看看在浏览器输入的内容:
http://welkin.online:8080/2017/05/?comment=0
以上的内容我们称作 URL 或者 URI (URL 和 URI的区别我们会附在结尾)。
我们来看看发出去的请求,用plain text
来看是什么样的:
GET /2017/05/?comment=0 HTTP/1.1
Host: welkin.online
Connection: Close
User-Agent: Paw/3.1.1 (Macintosh; OS X/10.12.5) GCDHTTPRequest
这个请求内容第一行的单词就是请求方法,这里我们使用了GET
方法来请求资源;第一行方法后面的就是请求路径和搜索,我们在这里请求了/2017/05/
这个路径的资源,并且给出了comment=0
这个搜索条件;之后的行数里面都是头部字段,我们可以看到第二行是Host字段,第三行是Connection字段,第四行是User-Agent字段。下面我们来分析一下这几个要素:
这里的协议就是 http://
。我们首先建立一个协议的概念:什么是协议?协议就是双方协定好了的一种数据的表达、通信或者编码方式。比如我们在桌面上新建一个空白的.html
文件,然后右键使用浏览器打开,这个时候我们还会看到http://
开头的协议吗?不会,我们会看到file://
开头的协议,这就是file
协议。那如果我们在浏览器地址栏中输入其他的协议呢?比如smb://
,ssh://
,ftp://
会怎么样呢?
截图:
smb://
,ftp://
这两个协议都会弹窗提示我们是否打开Finder.app
,而ssh://
则会问我们是否要打开终端.app
。**由此我们可知道在操作系统里面,不同的协议种类是对应到了不同的客户端软件的。**而网络浏览器主要对应的协议就是http://
和https://
(当然,也可以支持file://
协议)。
而我们使用的HTTP协议,是一个文本传输协议(Hyper Text Tranfer Protocol),它直接传输文本内容,并且不保留客户端协议。
这里的主机名就是 welkin.online
。它的作用是什么?由于使用互联网的用户越来越多我们的IP地址变得非常紧张,有的时候,我们不得不把好几个网站共享一个IP地址(即同一个物理服务器),这个时候我们把每个网站称为一个虚拟主机
而把物理服务器称为服务器
,然后使用诸如nginx
来作为一个http
服务软件。而域名在这里面除了能给DNS以查询IP的功效之外,还需要放在HTTP请求里面(host头字段中)方便服务器上的程序区分该请求究竟是请求该物理主机上哪个虚拟主机的。
这里的路径就是 /2017/05/
,这里是告诉http服务器我们想要访问的文件基于网站根目录的位置。假设在Linux服务器上,网站的根目录是 /www/
,那么我们在这里访问的目录就是/www/2017/05/
。后来我们有了PHP等后端程序,这个时候,目录也可以并不真实存在,而是变成了路由
。
这里的搜索就是 comment=0
,这个搜索是完全交给后端程序处理的。比如我们传递了一个comment=0
,那么后端PHP程序就会去找这些博文中,评论数量等于0的返回。这个方法在 HTTP协议 0.9 版本的时候,是向后端传递信息的唯一方法,因为 HTTP 0.9 仅支持GET
方法,并且不支持头部字段。
请求方法,也叫做HTTP动词或者HTTP谓词我们这次请求是通过GET
方法来请求的。那么是不是只有GET
这种方法了呢?当然不是,常见的HTTP方法和它们的操作如下:
GET: 从服务端获取一个资源(不对对服务端的数据做修改)
POST:在服务端新建一个资源(会在服务端的数据新增记录)
PUT: 更新服务端的一个资源(会修改服务端的数据)
HEAD:只返回GET方法的头部
OPTION:询问服务端支持哪些HTTP方法
DELETE:将资源从服务端删除
而我们最常用的请求方法就是GET
和POST
方法。一般情况下,我们使用GET方法从服务端获取资源,如HTML
文件,CSS
文件,JS
文件。而使用POST
方法来传递一些表单给服务端。
我们经常可以看见有的开发者在GET
方法的搜索中传递参数(这很可能是从 HTTP 0.9 时代带来的坏习惯),以至于习惯了之后竟然将用户名和密码的明文通过GET方法
来传输,造成严重的泄密事件。
我们来辨析一下这两个方法在传递参数时候的同异:
GET
方法:参数直接写在URL中,通过URL query(搜索)来传递参数,在某些浏览器上会有长度限制,且传递的参数会在途经的路由设备上的日志记录下来,如果传递用户名等敏感信息会导致信息被截获的风险加大。
POST
方法:参数写在请求的body中,可以传输的信息长度比GET
方法要长,不会存在被路由设备记录下信息的风险。
Q:既然不能明文传输,那我们应该怎么处理用户登录的账号和密码呢?(答案见课程最后)
每一个请求都有头部字段,比如:Host,或者 Connection。头部字段附带了一些详细信息,比如用户想要访问的域名,或者本次访问的连接类型,或者用户使用的浏览器类型。更多关于HTTP头字段的信息可以在维基百科中找到。在后面我们还会继续提到这个话题,所以这里就先不详细展开。
协议 -> 客户端与服务端协定的通信方式。
域名 -> 告知服务端的http
服务软件我们访问的具体是哪个虚拟主机
路径 -> 告知后端程序我们想要访问的文件路径(该路径可以不存在,只要后端程序可以处理即可)
搜索 -> 告知后端程序我们想要查询的一个内容,后端程序进行计算后返回结果。
请求方法 -> 告知服务端我们想要进行的操作
首先我们看看服务端的返回是什么样的:
HTTP/1.1 200 OK
Content-Length: 3059
Server: GWS/2.0
Date: Sun, 28 May 2017 02:44:04 GMT
Content-Type: text/html
Cache-control: private
Set-Cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S=SMCc_HRPCQiqy
X9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.welkin.online
Connection: keep-alive
<html>
<head>
</head>
<body>
Hello, world!
</body>
</html>
这段返回的前面几行怎么看着这么眼熟呢?是不是和请求的头特别像?没错,这个就是响应头
,第一行包括了HTTP版本和HTTP状态码(HTTP Status Code)
(更多内容更可以参考wikipedia),最后是HTTP状态码信息
。
服务端返回的文件的头几行一直到第一个空行为止,都是HTTP返回头。它包含了一些必要的信息。它也由头行和后面跟随的HTTP头字段构成。头行包括HTTP协议版本,HTTP状态码,状态码信息。
HTTP状态码`的一些常见数值:
一般此类消息表示服务端已经收到了客户端的请求,需要继续处理。一般这个是临时响应。
表示请求已经被客户端成功接受并且处理。常见的有200 OK
、201 Created
、202 Accepted
表示资源已经变更,需要客户端进一步操作,常见的有301 Moved Permanently
、302 Found
表示客户端的请求有问题,常见的有400 Bad Request
、401 Unauthorized
、403 Forbidden
、404 Not Found
、405 Method Not Allowed
、408 Request Timeout
表示服务端发生了错误,常见的有500 Internal Server Error
、502 Bad Gateway
、503 Service Unavailable
、504 Gateway Timeout
以上只是列举了一些常见的状态码,更多详细的信息可以参考维基百科
响应头的头字段也是用来描述一些不需要直接放到响应体中的信息。比如服务端使用的软件,或者响应的长度(字节数),但是最值得说的就是Set-Cookie
字段(还有即将提到的ETag
字段):Set-Cookie
是为了给HTTP这个无状态协议加上状态而提出来的一个内容。服务端在这里告诉客户端:“hi,下次你请求新资源的时候带上这个,我就认得你啦!”而客户端下次请求的时候带上了Cookie: cookieFromServer
,就能让服务端辨识出自己的身份了。
那么ETag
字段又是干什么的呢?这个字段反映了对于某个资源的某个特定版本的一个标识符,只要客户端发现了这个标识符没有发生变化,就不会再次和服务端请求这个资源,是我们用来提速的一个好东西。(后续在缓存策略中会有提及)。
那么回归正题,请求在服务器中,究竟是怎么走的呢?
网络请求打到服务器上之后,(如果是在网页服务器监听的端口上,比如80端口,的时候)首先会经过网页服务器,如果没有经过网页服务器监听的端口,那这个请求就会直接进入那个端口(比如说8080端口),如果这个端口是由后端服务器(如PHP,node.js)开启的,那个这些请求就会直接发送给后端程序。而进入网页服务器的请求,会按照网页服务器的配置来进行分流(如果在网页服务器上托管了多个后端程序,即托管了多个虚拟主机)
在平时开发中,如何学会给错误返回除错?
首先,我们要学会看请求是怎么被处理的。也就是说,这个请求究竟是首先被网页服务器处理了,还是直接发送到了后端程序?其次,我们再去看网页服务器和后端程序的日志文件。
一个典型的NGINX配置文件:
#user nobody;
worker_processes 1;
error_log /usr/local/var/log/nginx/nginx_error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log /usr/local/var/log/nginx_http_access.log;
error_log /usr/local/var/log/nginx_http_error.log; # debug;
sendfile on;
keepalive_timeout 65;
gzip on;
server {
listen 80;
server_name welkin.online;
charset utf-8;
access_log /usr/local/var/log/nginx/welkin.online.access.log; # main;
error_log /usr/local/var/log/nginx/welkin.online.access.log; # main;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
}
location /login {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
我们需要注意的是nginx整体的error_log
位于/usr/local/var/log/nginx/nginx_error.log;
,welkin.online
的access_log
位于 /usr/local/var/log/nginx/welkin.online.access.log
,error_log
位于/usr/local/var/log/nginx/welkin.online.access.log
。当我们排查问题的时候就可以按照这样的顺序去排查问题。
从上面的响应来看,服务器返回的仅仅是一个HTML
文件,那么网站里面的CSS
和JS
文件都是什么时候被返回到浏览器的呢?让我们跟随这串HTML
信息的比特流返回到客户端看看究竟。
随着电位变化的传递,我们跟随着HTML文件来到了客户端。浏览器一听到HTML
文件来了就开始了HTML解析过程(HTML Parse),解析过程生成的产物叫做DOM树
。在这个过程中,遇到了link
标签的样式文件,浏览器就会调动一个下载线程去下载和解析这个样式文件(下载和解析CSS文件不会阻塞HTML的解析过程),CSS的解析产物叫做样式表对象
。而遇到了script
标签的时候,浏览器却会停止HTML解析过程,转而去下载和执行script标签的脚本文件。这又是为什么呢?
如图,JS的加载之所以影响HTML的解析,是因为有DOM API的存在,哪怕HTML都解析完了,在最后的脚本中调用 document.write 方法也会重写页面内容,为了优化这样的情况,浏览器决定在碰到script标签的时候一定要先下载并且执行完脚本才继续HTML的解析过程。而CSS文件的下载,虽然不会影响HTML的解析和JS的下载,却会影响JS的执行。这是因为JS中有一些DOM操作是依赖CSS的,比如使用JS给获取某个DOM元素的宽度,那么首先这个DOM元素得有宽度,而很多时候DOM元素的宽度是从CSS中来的。
在我们的DOM树
和样式表对象
都生成之后(再次注意DOM树
的构建,JS脚本也会参与),由他们两再合并成渲染树
,浏览器再通过渲染过程
,把渲染树显示到屏幕上。从流程上来说,只要生成了新的渲染树,就会再次触发渲染过程
,因此,使用JS来改变某个DOM元素的样式,是会重新生成样式表对象
和渲染树
的。
到渲染过程
结束,我们大概的走了一遍从将地址输入到浏览器地址栏到最终页面在浏览器中显示出来背后发生的事情。那么我们的开发也是围绕这些展开的。接下来我们讲讲围绕性能,我们有哪些必知的。
假如我们的样式表,JS脚本都存放在北京的机房中,对于美国的用户来说,他首先需要从这个机房的服务器获取到HTML文件,然后在HTML的解析过程中发现CSS和JS文件,他不得不继续向北京的机房请求这些文件,让我们来算算时间吧:
北京到纽约的直线距离为16000KM,以光纤为例,假设折射率为1.5,那么往返一趟的时间为 16000KM/299 792 458 (m / s) / 1.5 = 35.58ms。以请求为例,一个请求至少需要3次握手,3次握手也就是 35.58*3 = 106.74ms。也就是说每个文件至少需要100ms来加载(这是最好情况!)。假设有3个CSS文件,那么除了加载HTML文件外,浏览器还需要300ms额外时间尝试从北京机房下载静态资源到本地。
那我们如果想要用户得到的响应更快,是不是应该拿出一些方法来?于是就有了 CDN 。CDN 将 CSS 、 JS 这些静态文件(为什么叫做静态文件是因为与可能是后端程序生成的HTML不同的是这些文件在开发的时候就被开发者写出来了)放到用户身边的机房去。
CDN的一些展开:缓存策略和CDN(要开始填HTTP响应头字段那里埋下的坑了,我们再回想一下,在HTTP头字段的时候,我们提到了一个叫做ETag
的头字段,表明它是对文件的一个标识符,只要标识符不变,就可以用本地的缓存)。那我们来看看 CDN 和缓存的一些联想吧?
从速度上来说缓存比 CDN 更快,哪怕是 CDN 也需要花费3次握手去获取文件,而缓存可以直接从用户的磁盘里面读取文件。那这又能怎么样呢?反正浏览器都花时间去请求远程的资源了(要不然就拿不到ETag
字段)。但是如果考虑到是一个大的文件,三次往返并不一定能拿到所有的内容,这个时候从缓存拿文件显然更加快。
而针对CDN的一些优化更有意思:比如在打包页面的时候,将所有的第三方库独立打包,将页面逻辑JS也独立打包,这样生成了两个JS文件,然后给库命名为 lib.{libhash}.js
,而逻辑文件命名为 main.js
,并且在HTML中写成 main.js?v={packTime}
这样能更好的利用CDN和缓存。下面我们来解析一下这两种明明方法的区别。
CDN在收到一个请求的时候,会首先看看自己的机器上有没有这个资源的快照,如果没有就向上游请求该文件。比如 main.js?v=201705
这个资源,假如CDN只有 main.js?v=201704
这个快照,那么他就会认定需要到上游请求一下 main.js?v=201705
这个资源。这样的话,只要我们改动了 HTML 文件中main.js
后面的v=
参数即可强制CDN刷新静态资源文件。而lib.{libhash}.js
则是为了充分利用缓存而进行的策略。浏览器在收到要请求lib
文件的时候,会首先看看本地的缓存,如果缓存没有过期,并且名字和URL相同的话,就会使用本地缓存。这样就能减少浏览器对CDN的请求,加快脚本的加载过程。以下是同一个资源在使用缓存和不使用缓存下的速度差:
我们可以使用 Chrome Dev Tool 的 Network 标签来对页面加载的一些网络情况进行研究分析,并提出改进开发实践的方案。Chrome Dev Tool 的 Network 标签如图。
Chrome Dev Tool -- network performance panel
样式表写到前面,让浏览器有时间提前去下载(这并不会阻塞HTML解析,反而会让样式表对象更快的构建出来) 阻塞DOM树构建的脚本放到页面后面,让DOM树先构建出来然后和提前下好的样式表构成渲染树先显示一部分
本章节内容主要是搭建一些关于安全的基本概念,从加密的原理到应用,到实际使用以及一些常见安全问题的认识。还捎带讲解了 ajax 和 jsonp 的基本原理。
这一章节主要讲的是密码学相关和东西,那么我们先看看一些经典案例。 泄密导致的很多都是丑闻,比如说陈老师的照片,如果他的电脑里面的照片至少使用一个加密文件保存的,或者他的电脑开启了整盘加密的话,那么可能职业生涯也不会遭遇重创。或者朴大妈的闺蜜如果电脑也开启了加密,那么也不会有今天下台的惨剧发生了
而就在前不久,Google宣布成功碰撞了 SHA-1 算法,它发布了两个内容不同的PDF文件,但是这两个文件通过 SHA-1 散列函数计算出来的 hash 值却一模一样。这个对于我们程序员来说有非常大的关系。但是这里我们首先不细说,留待单向散列函数处展开。
最明显的,密码学要解决的就是信息在传递过程中不被第三方获知信息内容。那么从原来的思路来看,有两个方法:1、将信息隐藏起来,称为隐写术;2、将信息加密起来,称为加密术。这就是密码学要解决的问题和两个解决方案。隐写术有很多例子,比如剃掉头发刺字,然后等头发长起来了再去接受信息方哪里。使用一本约定的书籍,将要传递的字符通过页码和字符的位置传递,这些方法都使得被传输的信息不实明显可见。而典型的加密术就有:通过一定的手段调换字符的位置,让传输过程中的信息变得不可解。到了接收方的时候,接收方再按照一定的方法将字符串调换的位置还原,即可读取信息。
从加密的概念引申出来的有三个基本概念:明文,秘文,密钥
明文就是传输方想要传送的信息。它一般十分重要,不可以轻易落到非接收方的手里。
密文是通过一定手法转换的明文,它的信息量等价于明文加密钥。
密钥是转换手法必须的东西,有了这个和密文,就可以解开明文。
从以上三个概念上看。密码学将“如何安全地将明文传送给接收方”变成了“如何安全的把秘文和明文都传输给接收方”(WTF!为什么使用了加密之后要传输的内容反而变多了呢?)
显然,我们在传递被加密过的信息的时候,既要把秘文传输给对方,还要把密钥传输给对方。一旦密钥被截获,秘文泄密的可能性就大增。这个关于密钥传输给接收方的问题称为密钥配送问题,即如何安全地将密钥发送给接收方。如果想要达到安全,那么最好秘文和密钥不要通过同一种渠道发送,否则很容易两者皆失,导致信息泄露。
古典密码学的一个最常见的方法就是凯撒密码
凯撒密码就是将字母表的全体向右平移N位,使用这个新生成的字母对应表来替换明文的内容。在凯撒密码里面,原来写好的信息就是明文,N就是密钥,而替换后的字符串就是秘文。凯撒秘密有一个明显的问题就是密钥空间小。只有25种可能性。只需要一点点的暴力尝试,就能破解凯撒密码的信息。凯撒密码属于一种简单的替换密码。
假如将26个英文字母每个都随机交换,这样生成的密码,强度大大强于凯撒密码,是否更加好呢?显然这种密码的密钥空间高达 25!
,是一个非常巨大的数字,想要收工暴力破解几乎是不可能的。但是我们现在显然没有在用这样一种加密方法,为什么呢?因为这种方法并不能抵抗频率分析。在一般的英文中,字母出现的频率是有差别的:比如字母 I
E
的出现频率非常高,而字母 Z
的出现频率却特别低。在一些文章里面 the
这个单词的出现频率也是大大的高于其他单词。通过这些信息,在一份足够长的信息中,就可以照出里面字母对应的规律,从而破解密码。破解的人,并没有通过暴力破解的手段来完成这个工作,而是通过字母频率,单词频率找到了通向破解的钥匙。
由于替换一次不能抵抗频率分析,那么可不可以多次替换呢?维热纳尔密码就是这样一种密码,从图示结构上看的确不好懂。但是我们可以这样理解,它是将明文按照特定长度分组,然后组内每个位置都有一个自己对应的替换表。这样就好理解了。它大大降低了频率分析的攻击风险,因为同一个字母,在不同的位置上,可能被替换成不同的字母!这样就破坏了秘文字母和明文字母之间的对应关系,导致针对字母的频率分析失效了。
那么,还有没有办法破解维热纳尔密码呢?
答案是有的!
一个很明显的地方就是在于它的“分组”。每个分组同一个位置对应的替换方式是一样的。那我们就要抛弃传统的统计字频,而转向统计重复位置。假如分组长度为3,teeths
中的两个 t
就会被相同的替换方式来替换,那么他们得到的秘文字母也应该是一样的。通过寻找这种重复项,我们可以推测替换的秘文长度,再通过对这个秘文长度的每一个位置的字母收集起来做频率分析,就变成和简单替换密码一样的破解方式了。
但是这个密码还是有用武之地的,前面我们用3位长度的密码,很容易被破解,那么假如我的密码长度有10个呢?20个呢?100个呢?这个时候有可能对密码长度的分析都不奏效了。这就是后来 Enigma密码机的一个雏形。
到了这里必须要讲讲二战中德国使用Enigma密码机了,正是针对这个机器的破译工作让图灵成名,并且启发了现代计算机科学。Enigma密码机使用了机械转盘和电路,完成了非常复杂的替换加密,达到了人类手工无法创造和破解的高度(是不是和最近的AlphaGo有一点点似曾相识的感觉呢?)
从硬件上来说 Enigma密码机有以下几个部分组成:
- 接线板
- 转子
- 输入按钮
- 输出灯泡
从使用概念上说 Enigma密码机包括几个不同的要素:
- 每日密码,由国防部预先制订好了一个月内每天使用的每日密码,印制发放给所有使用者。由于是统一印制的,所以所有人在同一天实用的每日密码必定是相同的。每日密码决定了第一级加密接线板的链接方式,实质上是用来加密2项目中的本次通信密码。因为被发送出去的本次通信密码是在每日密码接好的基础上,连续两次输入通信密码得到的秘文。而 Enigma机器有一个特性就是,将加密后的秘文输入,就可以得到原来的明文。在使用的时候,收发双方都按照每日密码来设置 Enigma密码机并且把机器的转子都归位,这样双方的机器都是一样的配置。
- 通信密码,三个字母,决定了今天所有通信使用的密码是什么密码加密的。将本次通信密码两次输入已经按照每日密码设置好的 Enigma密码机上,生成加密后的秘文。比如:
abcabc
生成了dsgitk
。那么通信密码就是abc
,为了防止无线电出问题,重复两次就是abcabc
但是在发送的时候是dsgitk
。 - 加密/解密操作:将需要加密(解密)的字符串按顺序输入到已经配置好的机器中,记录下输入灯泡显示的字母,就是加密(解密)后的秘文。实质上加密和解密的操作都是一模一样的,这样大大节约了时间。
但是从这些上面来说,Engima密码 //段落缺失
一次性密码本可以理解为更进一步的维热纳尔密码,唯一的不同之处在于,密钥分组长度和通信长度一样。这样的话,无从针对分组开始分析,找不出重复的分组,就不可分析。而生成这个和通信长度相同的密钥是随机的,而想要生成这么长度的随机密钥
是非常困难的,即使是人工生成也会不自觉地带上自己的一些模式,实际上是不随机的。真正的随机是比如自然界发生的真正不可预测的事件:例如放射性元素镭释放中子的时间,或者大气的噪音分贝。(目前互联网上有提供这种随机数的服务,如:(参考某个使用噪音来生成随机数的))
小知识:随机数与伪随机数算法
众所周知,密码的一个需要具备的特性就是不可预知性。也就是说,别人不能轻易猜测到。而最符合这个条件的,我们可以想到使用随机数。(吐槽一下,其实随机这个词拆开来看就不是很好,听起来就像是让机器来就定的数字,那万一机器只能生成一个数字,不是就不‘随机’了吗?)自然界中随机的事情很多,比如前面提到的放射性元素释放中子的时间和数量上就是随机的。但是我们不能把一块放射性元素做成一个生成随机数的生成器吧?而且就算做成了,那也远远不够我们用的。于是就有人聪明的发明了伪随机数算法。
伪随机数算法就是通过给定不同的“种子”,计算出一个随机数来。常见的线性同余
算法,就可以将给定的随机数种子生成随机数。这个算法有一些特性,第一,不能从结果推算出输入;第二,对同一个输入只产生一个输出。因为这个算法依赖输入,那么一旦我们的输入被人推测了,这里生成的随机数也可以被人重现。
统计学伪随机性:生成的随机数在统计学上均匀分布,比如一个十进制数中,0-9每个数字出现的次数相近,组合如01,02,03….09出现的次数也相近,等等。
我们走过了前面古典密码的基础铺垫,终于迎来了计算机时代,这个时候,我们处理的数据,文件,信息都是用二进制表示的了!那我们接下来的操作,就都是对二进制数据流进行操作。
一切皆文件。
而文件都是二进制数据,无论多大的文件,或者多长的信息都可以用一个二进制字符串来表示。而我们研究的就是对这一串二进制字符串进行加密和解密。
前面提到的 Enigma密码机有一个显著的特点就是加密操作和解密操作都使用同一个通信密码,并且连操作方式都几乎是一样的。我们要学习的对称加密也有这样的特性:
加密解密双方使用同一个方式,同一个密钥,来进行加密解密操作。那么在发送信息的时候,发送方还需要想办法将密钥发送给接收方,这样的话,我们还是面临之前提到的密钥配送问题。
为了解决这个问题,人们又发明了密钥服务器这样一种服务,让通信双方在通信的时候使用该服务器来商定密钥,最终的密钥也由该服务器确定之后发送给双方。但是这样,就会存在服务器可能被攻破的可能性。
为了解决密钥配送问题,有人就想出来了一种模式:我们能不能生成两个密钥,一个可以公开发送到互联网里面,另外一个自己秘密保留。而一条信息由其中一个密钥加密后,只能用另外一个密钥解密。只要用用开密钥加密过的内容,只能用秘密保留的密钥揭秘。这样,只要每个人都生成一对这样的密钥对,我们就能和任何人安全的交流了。这样的加密方式就将传统的密钥配送问题解决了(或者说转换成了密钥辨识问题,即接收方如何校验密钥是否是正确的。)
非对称加密又被称作公钥加密,指的就是它这种有两个密钥的特征。非对称加密是本章接下来的内容的要点。非对称加密是HTTPS,证书等的基础。
前面给大家看了,Google费了那么大劲碰撞的那个sha1
又是什么?这些都叫做单向散列函数
。从这个名字就能听出来是干嘛的。它拥有一下特性:第一,单向性,也就是可以从原文生成这个散列值,但是绝对不可能从散列值还原出原文,第二,唯一性,给定一个原文,只能生成一个散列值,散列值也可以认为是输入信息的摘要
。(是不是和上面提到的随机数算法很像呢?)
单向散列函数
将一串二进制数据与一个特定的结果建立个一个单向的唯一的映射。这个就是消息认证码的功能。只要任何人拿到了一个二进制数据,就可以使用单向散列函数
来重新验证这个映射。这个操作将消息的未经篡改问题转换成了单向散列函数是否符合单向性和唯一性的问题,而后者更通用也更容易证明。**也就是说,在单项散列函数的两个特性成立的情况下,只要接受到的信息和信息摘要能够用单向散列函数校验,就可以说明这两个信息是一定没有在传输过程中发生被篡改或者丢失的。**谷歌在sha1函数的碰撞上就是证明了sha1单向散列函数的唯一性是有问题的。
消息认证码可以解决判定消息在传输过程中是否被篡改,那么针对消息认证码有什么没有攻击手段呢?
答案是有的,消息认证码是不能防止伪造
和抵赖
的。
伪造:假设有一个攻击者,他不停的伪造发送方的消息,并且附上这个消息的认证码,这样接收方也能校验这个消息,但是这个消息不是来自真正的通信方的!
而且接收方没有办法辨认。
抵赖:发送者对之前发送的消息不想承认,而宣称这个消息是由攻击者发送的,由于不能确认这个消息就是发送者发送的,所以这样的抵赖是有可能发生的。
前面我们在消息认证码中提到了,消息认证码虽然能校验消息在传输的过程中是否遭受了篡改,但是却没有办法确认消息的来源,数字签名就是为了解决这个问题而提出来的。数字签名和非对称加密的特性紧密相关:私有密钥的私密性以及用其中一个密钥加密的消息只能用另外一个密钥解开。
数字签名的大致操作如下:接收方通过一定途径获得发送方的公开密钥;发送方将信息生成一个摘要;发送方使用自己的私有密钥将此摘要加密;发送方将加密后的信息和加密后的摘要发送给接收方;接收方使用发送方的公开密钥对消息摘要的秘文进行解密获得摘要A
,并且将接收到的消息重新生成摘要B
;接收方校验摘要A
和摘要B
是否一致。
由于私有密钥的私密性这个条件存在,我们可以认为这个消息摘要的秘文一定是由发送者本人生成的。而用其中一个密钥加密的消息只能用另外一个密钥解开就是校验是否是使用发送者私有密钥生成摘要的依赖。
当然,如果发送者宣称自己的私有密钥丢失的话,依旧是可以抵赖的。但是如果总是宣称自己的私有密钥丢失的话,发送者的可信度就会相当低,试问谁会愿意和一个总是粗心大意的人经常来往呢?
数字证书也是一种数字签名。在数字证书的操作中,信息的生成者(发送者)和给消息摘要加密的加密者并不是同一个人。数字证书中有一个角色叫做证书机构:证书机构是一个由大家信任的权威机构,它的公钥几乎所有人都有(至于怎么有的会在后面讲到),它的私钥则非常安全得由它自己保管。(其实这个机构对于自己私钥保管的严密性和使用的严谨性就是证书机构可信度的一个指标,对自己私钥保管不好或者随便给别人使用自己私钥签名的证书机构不是好机构,之后的Wosign为什么不可相信也会在这上面论述)。
好了,该到了证书这个重要的概念了:假设有一个服务商A,他想要给自己的服务器加一个证书,已标示自己就是那个A服务商,他可以向某权威证书机构申请一个证书。证书是A将自己生成一个公钥发送给证书机构,证书机构将此公钥生成一个摘要,并且用自己的私钥将这个摘要加密生成Z
,再将加密后的摘要Z
发还给A。这就完成了证书的申请过程。那么证书是怎么在实际的网络中生效的呢?
在使用过程中,用户访问A服务商的网站,A将自己的公钥和加密后的摘要Z
发送给用户,告诉用户说,你可以相信我是某权威证书机构认证过的A,没有仿冒,之后咱们的通信要使用我的这个公钥,你保管一下哦。因此只要你过用户相信权威证书机构,那么用户对权威机构的信任就会传递到对A的信任来。
因此最后证书将问题归结到了,你是否相信这个权威的证书机构了,那么话说回来,我们凭什么相信这个证书机构,用户即没有见过它,也没有和他通信过,凭啥呢?
原来,在我们使用的计算机操作系统被释出的时候,操作系统里面已经内置了 很多可信的证书机构的公钥了! 这样的话,我们的计算机在操作系统被安装的时候(甚至在OEM安装的时候),就绝对相信这些证书机构了(当然,某些OEM也是坏坏的,此话后讲。)。举个例子,在《人民的名义》中,大风厂的工人最相信的人就是陈岩石检查长,那么他们对陈岩石检查长的信任也会随着陈岩石对别人的信任而延伸,比如陈岩石坚信侯亮平是无辜的,那么大风厂的工人们也会间接地相信侯亮平是无辜的。同理,只要用户相信了证书机构(即用户计算机上安装了证书机构的公钥), 就代表用户完完全全相信这个证书机构所相信的任何东西。 这是非常大的信任。但是有的攻击手段就是从这里来的。比如之前说的OEM作恶:联想电脑出厂内置的广告软件安装第三方根证书。通过这个根证书,由于使用了弱口令,就被攻破了,导致黑客可以使用该根证书为任何伪造的证书签名,想象一下你访问的淘宝,看到有证书,就没有细看,结果这个证书是假的,那么之后的购买都在虚假的网站上进行。或者更加危险的如Wosign证书管理混乱问题。苹果关于取消信任Wosign证书的申明;Mozilla不再信任任何 Wosign 和 StartCom 颁发的新证书。
HTTPS与证书紧密相关。HTTPS是对HTTP的一个加强,主要就是体现在加强了通信的安全性。HTTPS为HTTP加上了一个TLS/SSL的加密层,其余的方式和HTTP并没有改变。我们来看看HTTPS是如何混合了对称加密和非对称加密的:
参考图HTTPS的握手通信:
HTTPS的大致思想:首先双方通过非对称加密来交换一种双方都可以接受的对称加密的方式和密钥,这个就是TLS/SSL的握手过程。在商定结束以后,双方切换到对称加密来通信。为什么这么做?因为前面提到了,非对称加密的运算速度是缓慢的,远不如使用对称加密,但是对称加密又存在密钥配送问题这个致命缺陷,于是我们就想到用非对称加密来解决对称加密的密钥配送问题的死穴。
小知识:
2017年5月12日全球爆发的 WannaCray 病毒和HTTPS拥有类似的模式,你能想想他是怎么做的呢?
这个病毒首先使用对称加密来加密用户的文件,再将这个对称加密使用的密钥通过病毒开发者的非对称加密的公钥来加密。这样的话,只能使用攻击者的私钥来解密。攻击者也不需要存储被攻击者的电脑信息和对应的密钥。勒索的时候要求被攻击者将需要解密的密钥文件发给攻击者,攻击者解密后再发还给受害者,受害者就可以使用这个密钥来解除文件的锁。在实际操作中,受害者在勒索软件中输入了自己缴纳的比特币信息,勒索软件将比特币信息连同被加密的密钥发送给攻击者,攻击者收到消息后确认收钱,解密密钥,密钥传输回勒索软件,勒索软件将用户的文件解密。
那么,假设病毒预制的共享密钥长度为2048位,那么在一台运行速度为3GHz的计算机上,需要多久才能暴力破解这个密码呢?
CSRF攻击是一种伪造请求攻击,通过一些途径,让用户不知不觉发送出了请求,就是CSRF攻击。举个简单的例子:某用户A登陆过微博之后,微博在他的浏览器保存了cookie来标示他之前登陆过。攻击者M制造了一个网页,里面有一串恶意代码,能在该页面内发送一个发微博请求给微博。由于这个请求是发送给微博的,所以浏览器就会自动带上微博的cookie,这样,恶意攻击者不需要获取到用户的账号密码或者cookie本身,也可以操作用户发送用户完全不知情的微博。同理,这也可以用来攻击网银系统。
那么这种攻击有办法防护吗?答案是可以的,只需要服务的提供商(如微博,网银)在接收到请求的时候加上一个校验。那么我们怎么校验呢?比如说在用户正常访问的表单内,插入一个不可见的表单内容,如csrf-token
,并且在后端渲染页面的时候就给这个不可见的表单项加上一个随机生成的字符串。在用户发送请求的时候,校验一下这个表单项是否是渲染的时候给出的,这样就可以简单的防护用户不知情发送的一个请求。
XSS攻击叫做跨站脚本攻击,一般发生在对于用户输入没有过滤的时候。假如微博对用户发的微博没有做一些过滤,攻击者M在自己发的微博中嵌入了一段script
标签,当这条微博被显示到时间线上的时候,任何访问的人的浏览器就会执行这个脚本。对于XSS攻击的一个简单的防护手段就是将用户的输入转码。特别是一些特殊符号。
本章内容主要讲解了一个实例,来自 UBER 的 API 分析。以及粗略的了解了 百度短网址 iTunes Search GitHub API等一些常见 API 的使用。
从这个API的实际上来说,我们可以看到几个典型的特点:
使用HTTPS来做传输协议,这样就能在很大程度上保证数据的安全性。
使用authorization字段来做身份认证,以免资源被人盗用。
通过URL来区分资源
有些有专门的域名 比如 api.github.com
使用HTTP动词来表述对资源的操作
大量使用JSON
而设计不好的API就会带来问题,比如前文在学习HTTP方法的时候,如果一个API将机密要紧的信息放到URL query中传递,就是不好的。
接下来我们实际体会一下 百度短网址的API:
curl -X "POST" "http://dwz.cn/create.php" \
-H "Cookie: BAIDUID=4805FE7928FD44BCD1D491D1DE97F304:FG=1" \
-H "Content-Type: multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__" \
-F "url=http://weibo.com/ttarticle/p/show?id=2309404087394109359528#_0"
使用GitHub API查询某用户信息:
curl "https://api.github.com/users/qinyuhang" \
-H "Cookie: logged_in=no"
使用iTunes API查找 iTunes 商品信息:
curl "https://itunes.apple.com/search?term=replica-web&country=cn&entity=software"
我们注意到只有百度的请求是使用了 POST 方法,为什么呢?原来,我们在百度的服务器上创建了一个 我们长链接和百度短链接之间的映射关系,那么使用POST方法来表示创建资源,也是可以的。而余下的则使用了GET方法,因为我们只需要从服务端获取需要的信息,并不需要修改或者创建资源。
以上~