《凤凰架构-构建可靠的大型分布式系统》读书笔记 -关于架构安全性的一些总结
前言
《凤凰架构-构建可靠的大型分布式系统》中有一些内容是关于网络请求中安全相关的一些常见主题,比如认证、授权、凭证、加密等等,很多内容和开发息息相关,有一些是业界较为通用的成熟安全设计方案、被很多公司都采用(比如说微信第三方平台的认证、授权、凭证方案),这里做个总结,希望可以便人便己。
认证与授权
认证
网络通信服务中,认证主要是为了判别请求的用户是否有权限来访问对应的资源。即判断**"你是谁"**。
对于具体的认证实现过程来说,不同的协议有不同的认证协议逻辑,也对应这不同的认证安全级别,但站在抽象的一层来看,认证的实现依赖认证双方达成一个共识。这个共识可以是普通的密码,也可以是加密之后的一段数据,也可是认证证书... 同时,这个共识达成的过程可以是直接发送(用户直接发给认证方),也可以通过请求协议发送...
不同服务协议有不同的认证方式,比如说常见的SSH可以通过密码认证,通过公钥认证 ;

下面简单讨论最常见的http服务中用到的一些认证方式。
基于信道的的认证

基于信道的认证主要指的是在建立通信之前,要保证通信链接的安全。如果底层的通信信道不安全,那么下面所谈的其他认证方式的安全性都会大打折扣(无法避免中间人攻击) 。
HTTP协议中典型的信道安全协议就是增加一个TLS安全层,也就是HTTPS协议。 在新增加的安全层中先通过非对称加密的方式协商出一个双方的共识秘钥 ,然后后续的通信通过这个共识秘钥进行对称加密。
具体的HTTPS过程网上有很多,这里就不赘述了。
基于通信协议的认证
Http协议原生就支持认证方案,一般来说具有统一的认证框架。如下图所示:

当客户端请求资源时,如果没经过认证,服务器会返回401,并在返回的Header中 用WW-Authenticate 来告诉客户端具体的认证方式。然后客户端产生认证之后,向服务侧重新发起请求。
Http协议原生支持的有如下几种认证方式:
-
Basic 认证: 这是最基本的认证方式,基本上只作为演示使用。因为它的密码通过Base64编码后直接通过明文传输,安全性没法保障。过程如下图所示。

-
Digest 认证: 这种方式也叫做摘要认证。通过使用把用户名、密码等加盐(nonce)然后通过MD5/SHA 进行Hash 取摘要发送验证。基本过程如下.

其中客户端返回的response的计算方式如下:
bashresponse = MD5(HA1:nonce:nc:cnonce:qop:HA2) HA1 = MD5(username:realm:password)(客户端预计算的用户密码哈希)。 HA2 = MD5(method:uri)(请求方法与资源路径的哈希,如 GET:/sd)。 nc:请求计数(Nonce Count,防止重放攻击的递增序号)。 cnonce:客户端生成的随机数(Client Nonce,增强随机性)。可以看到这种方式即使nonce被截获了,也可以防止重放攻击。
-
Bearer认证
Bearer认证的核心是token ,它通过服务器(一般由一个专门的授权服务器来充当)颁发的令牌(Token)来代表用户身份。客户端拿到令牌之后,后续请求在Authorization中携带token,这样服务端就可以知道此请求是经过认证的。bashAuthorization: Bearer [BEARER_TOKEN]通过Token认证的好处在于拿到token之后,后续不用传递密码(或者向Digest那样以密码加密的方式传递);缺点也很明显,令牌无法主动失效 ,因此令牌如果被窃取了,在一定的时间范围内,可能会被恶意调用请求。
这里需要说明的是,Bearer认证的令牌是可以任何格式的字符串,认证校验的逻辑转给了服务端。由于OAuth2和微服务的发展,实际上JWT成为了实现Bearer令牌的最流行、最常用的编码方式. 这个后文会说到。
基于内容上的认证

表单认证使用户在生活中最常见的一种认证方式,最简单的账号密码登录就是一种表单认证。它是依靠内容而不是认证协议来实现的认证方式。(上面的几种都是协议层面的认证) 对于大部分系统来说,用户想访问信息系统里的某些服务,肯定是希望认证系统而不是协议来完成,因此表单认证到目前为止占据了主流。
授权
一般来说,当认证通过之后,系统会对认证的用户进行一定程度的授权,即"你具有哪些权限?能访问哪些资源? " 。 对于简单的系统来说,用户->权限的单级映射也许可以。但是稍微复杂一点的系统,这种单级映射就会相当复杂。 所以,常见的认证授权系统中会在用户->权限中间新增一层角色(Role)的概念, 用户属于某些角色,角色拥有一定的权限,即用户-> 角色-> 权限,这也就是常见的RBAC 模型。

详细的RBAC模型概念这里就不赘述了,网上有很多资料,也可以参考【1】,【1】中讲的也很详细。
第三方应用认证、授权(OAuth2)
这里简单在讲下第三方应用的经典认证授权过程。所谓第三方认证,讲的是如果用户C正常使用服务S0。然后在使用服务S1的时候,S1的产品逻辑需要使用用户C在S0中的相关数据或者权限。 这个时候一般S1就会向C申请其在S0上的认证授权。这里的第三方应用就是S1。
基本的框架流程如下图所示。

OAuth2就是一种面向解决第三方应用的认证授权协议 ,它的核心是用"令牌 (Token)"代替"密码(直接把密码给第三方应用一般来说不是一个好的方案)",让第三方应用"拿着令牌去办事",而不是"替用户登录" 。 OAuth2一共提出了四种不同的授权方式,分别是授权码模式、隐式授权模式、密码模式和客户端模式。这里仅介绍一下我在工作中接触到的,用的比较多的,也是最严谨的授权码模式。很多第三方平台的认证协议用的都是这一种。
授权码模式的基本流程图如下所示:

一般来说,在第三方应用在申请授权之前,会到授权服务器(或者原始服务S提供的开放平台)上进行注册,提供一个域名地址、有些还会进行需要的权限登记。然后授权服务器会返回一个ClientID和ClientScret。(有可能授权服务器提供的ClientID和ClientScret是以事件推送的方式提供)
剩下的授权过程如下逻辑所示(这里书中讲的比较详细,直接引用过来了(# ^ . ^#))。


目前微信的第三方平台(服务商)授权(比如说第三方服务商想要获取用户公众号的授权)和 企业微信的第三方授权大都是采用这个思路。如下图所示:
这是微信的第三方服务授权。

这是企业微信的第三方授权。

是不是和OAuth2的授权码模式很像...
凭证
凭证和前面的认证、授权其实是有些关联的,当我们认证通过之后,后续的请求需要通过某种方式来保证请求是之前认证的那个客户发过来的,所以每次请求的时候一般会携带一些内容,以便服务端进行校验(其实从具体实现上来看,前面说的认证和凭证是有些重合的)。
这就涉及到一个比较经典的问题,用户的状态维护在哪儿,如何承载认证授权信息,是放在服务端还是客户端。
Cookie-Session 机制
如果是服务端的话,对于HTTP协议来说,经典的凭证就是就Cookie-Session 机制 。这种方式状态信息都存储
于服务端,依靠客户端的同源策略 和HTTPS传输层的安全性,可以保证 Cookie 中的键值不被窃取而出现被冒认身份的情况; 这种方式还有一个优点是,服务端具有主动的状态管理能力,可根据需求清除任意上下文信息,比较容易的视线类似强制用户下线这样的功能。
Token机制
如果是客户端的话,经典的方式是使用Token方式,不过这里的Token需要携带充足的想下文信息,以便让服务端进行校验(这样服务端就不用再维护状态了)。 目前广泛使用的一种令牌格式是JWT(JSON Web Token)。

具体的JWT格式这里就不赘述了,可以参考【4】。
使用JWT 这种token 凭证而不是cookie的一个好处在于其避免了cookie不能跨域的问题;其次由于token里带了状态信息,服务端可以不通过远程调用来校验token(比如说授权服务器 返回的token 用私钥进行签名,资源服务器用公钥进行解密,如果解得开,那就没问题;当然很多系统也直接又走了一遍授权服务器的校验);
不过jwt token也有一些缺点,比如说令牌难以主动失效(可以设置时效,但再时效范围内一般也无法主动失效);由于HTTP header的限制,jwt token只能携带有限的信息等。
数据的保密
保密指的是信息在传输或者存储的过程中,为了防止被窃听、泄露、篡改所做的一系列防范措施。最常见的要数在用户登录的时候对用户密码的传输的保密工作。下面是一些经典的保密手段:
-
以摘要代替密码明文。 对密码做摘要,可以防止即使泄露了密码,可以降低被逆推出原信息的风险 (对于有些简单的密码和简单的摘要算法,理论上通过最终的摘要破解出原始的密码。尤其是对于弱密码来说,还是有被彩虹表攻击的风险,这也是大部分系统让我们设置密码的时候,要复杂一些的原因)。有兴趣的,你可以用md5 对一个简单的密码做摘要,在【5】中没准可以直接查到对应的密码。
-
加盐值来应对弱密码。通过加盐可以在一定程度上防御已有的彩虹表攻击。但是不能阻止加密结果被监听后冒认。
加盐值有静态盐值(或者伪动态盐值)和动态盐值两种方式。如果是使用静态盐值(伪动态盐值)的话,万一盐值被泄露,依然有可能被攻击者针对固定盐值来做一套彩虹表,密码依旧会泄露。 动态盐值到是可以防止彩虹表攻击,也可以防止冒用,但是每次请求都要服务端来产生一个动态盐值,然后要保密的下发到客户端(服务端下发的盐值依旧有可能被监听),逻辑就会就会比较复杂。
-
使用HTTPS来保证通信链路的安全。HTTPS可以防御通信链路上的数据泄露、监听和中间人攻击。 但是如果从端上被攻破伪造根证书,这样HTTPS也无可奈何。
-
针对上面的风险,要求比较高的金融、军工等场景会使用物理的U盾来避免根证书被客户端中的恶意程序非法伪造窃取 。还有一些关键企业(国家电网、军事机构)会建立专门的内部网络来进一步隔离被攻击的风险。
需要说明的是,保密级别越高需要越高级的保密措施,响应的也需要更多的代价,在实际使用时要根据具体的使用场景来选择,简单的后台管理系统,没必要搞得像军事核设施那样复杂。
客户端加密
讨论一个最场景的场景,对于用户的注册、登录时输入的密码,客户端是否需要加密来保证防止泄密? 一般来说是需要的,但是客户端加密并不是为了防范客户端到服务端这个通信链路中的安全 ,因为对于大部分的系统来说,中间的链路是无法确定的,没有采用专门的通信链路层安全防护的话,很多地方都有可能被攻击、被窃取(各种代理、路由器、DNS服务等等...)。所以更安全的做法是对通信链路进行加密,对于HTTP服务来说就是HTTPS。
那为什么还需要客户端进行加密呢? 客户端进行加密主要是为了方法在端上的密码泄露,通信链路上可以通过HTTPS来保护,但是端上,尤其是服务端上如果密码没加密, 直接输出到日志中,或者明文存储到数据库中,那万一服务端被攻破,用户的密码数据就被泄露了。
通信链路上的加密
先简单介绍下通信链路上用到的一些加密技术: 摘要、对称加密和非对称加密。
-
摘要就是将输入的数据通过hash算法映射成一段不可逆、不可解析 的固定的输出数据(一般有固定的位数,如32位,128位,256位等)。 hash算法除了不可逆,不可解析之外,还有一个就是易变性(输入点发生了一点变化,可以引起输出端极大变化)。这样就可以辨别输入数据的真伪。
-
对称加密。对称加密是通信双方使用同一个秘钥进行加解密。它相对来说,运行较快,且加密的明文长度不受限制。 看起来不错,但是限制其使用的一个问题就是通信双方如何在不完全可信的网络中协商出这个公共的秘钥。
-
非对称加密。 非对称加密将秘钥分为公钥和私钥。公钥可以在网络中传输,私钥由使用方自行保管。公钥加密的数据,只能由私钥解开;私有加密的数据也只能有公钥解开。这样就解决了秘钥分发的难题 。但是非对称加密有一些本身的不足,一个是加密过程复杂,是性能要比对称加密低几个数量级。二是其加密的数据有限,加密的明文无法超过秘钥的长度。

一般来说对于非对称秘钥有两种使用场景:
-
公钥加密,私钥解密。这个就是加密过程。 A用公钥加密后的信息传输给B后,只能由B的私钥来解开。 由于其加密的长度有限制,所以一般用于混合加密中协商出 对称加密的秘钥 。
-
私钥加密,公钥解密。 这个就是签名的过程。 用私钥来对某些信息进行签名,拿着公钥的如果解开了,说明信息是可信的(因为被认证签名过了)。由于加密的数据长度有限,一般是对信息先取摘要,然后对摘要进行私钥签名。
-
关于数字签名
上面说的签名过程还有一个问题,对于简单的情况,可以通过其他方式来确认公钥的正确性(比如说在ssh协议中,如果人为把publickey加到服务器端的authorized_keys中,那么ssh登录验证的时候经过私钥签名的数据,只有公钥能够解析;这也算是通过签名来达到的一种免密登录方式)。
但是对于网络双方的服务调用中,经过A签名的数据发送给B之后,如果保证B含有的A的公钥是可信的呢?如果公钥在网络传输的过程中被截获并且篡改 ,返回了攻击者自己的公钥,那么后续攻击者就可以用自己的私钥来签名,然后让B来信任攻击者的身份了。 这个就用到了一种基于权威公证人的信任方式,也就是签名证书。 证书是权威机构对特定公钥信息 的一种载体,表明了权威机构对这个公钥的签名背书。
在验证签名的时候会涉及到几个公私钥的概念,这里简单介绍区分下。
我们假设钥验证服务端的可靠性,当客户端收到服务端的证书后,证书中包含了一个认证机构的签名和站点的公开密钥 。如下图所示。

这个签名就是权威认证机构(自认证其实也可以,但是不会通过校验)用权威机构的私钥(注意是权威机构自己的私钥)进行的加密签名,保证证书发放之后没有被篡改 。

然后浏览器收到之后,通过权威机构的公钥(预先安装在操作系统或者浏览器上)来进行解密,如果解密成功,则说明签名没问题 (实际在验证过程中会涉及到证书链的验证,这里先不考虑了)。 到此就是私钥加密,公钥解密的签名验证过程。 证书没问题,也说明了证书里站点的公钥是没被篡改的。
关于HTTPS的秘钥协商过程
HTTPS其实就是HTTP+TLS层。在开启HTTP传输之前要协商好加密算法、生成秘钥、公钥分发、CA认证、签名认证等一系列内容,这些都是TLS层来完成的。其本质就是先混合加密,先通过非对称加密协商出 一个公共秘钥,然后通过这个公共秘钥来进行对称加密。
下面简单介绍HTTPS的秘钥协商过程,主要关注秘钥协商相关的内容。

如上图所示:
- 客户端浏览器首先发送Client Hello 信令,传递一些可以可选择的密码套件和生成的随机数(随机数稍后用来生成秘钥)。
- 服务端收到之后,检测是否支持客户端的相关密码算法组合,然后回应选择的密码套件、服务端产生的一个随机数(稍后用于生成秘钥)、服务端的证书。
- 客户端收到回应之后,首先验证服务端的证书(通过上节介绍的方式)。没问题之后,发送第三个随机数,不过这个随机数不是用明文发送,而是通过证书里带的站点的公钥进行加密传输,也叫做PreMasterSecret.(这个PreMasterSecret 会和前面两个随机数一起生成最终的秘钥)
- 服务端收到最终的PreMasterSecret之后,根据自己的私钥来进行验证。通过之后,发送响应。至此,通信双方都拿到了3个随机数,然后通过算法可以确定对称加密的秘钥。
后记
上面很多内容在业务开发中都能遇得到,很多看起来是习以为常的东西,但是如果换一个角度来细细分析,还是有一些不了解的东西。《凤凰架构-构建可靠的大型分布式系统》这本书确实是本好书,作者的功底也着实深厚,点个赞。
参考
【1】《凤凰架构-构建可靠的大型分布式系统》
【2】HTTP Digest 认证:原理剖析与服务端实现详解
【3】《图解Http》
【5】CMD5解密