互联网架构安全性(二) 凭证与保密篇

1,507 阅读26分钟

白菜Java自习室 涵盖核心知识

互联网架构安全性(一) 认证与授权篇
互联网架构安全性(二) 凭证与保密篇
互联网架构安全性(三) 传输与验证篇

在 OAuth2 的内容中,每一种授权模式的最终目标都是拿到访问令牌,但从未涉及过拿回来的令牌应该长什么样子。反而还挖了一些坑没有填(为何说 OAuth2 的一个主要缺陷是令牌难以主动失效)。这节讨论的主角是令牌,同时,还会讨论如果不使用 OAuth2,如何以最传统的方式完成认证、授权。

1. 凭证(Credentials)

“如何承载认证授权信息”这个问题的不同看法,代表了软件架构对待共享状态信息的两种不同思路:状态应该维护在服务端,抑或是在客户端之中

在分布式系统崛起以前,这个问题原本已是有了较为统一的结论的,以 HTTP 协议的 Cookie-Session 机制为代表的服务端状态存储在三十年来都是主流的解决方案。不过,到了最近十年,由于分布式系统中共享数据必然会受到 CAP 不兼容原理的打击限制,迫使人们重新去审视之前已基本放弃掉的客户端状态存储,这就让原本通常只在多方系统中采用的 JWT 令牌方案,在分布式系统中也有了另一块用武之地。

Cookie-Session

HTTP 协议是一种 无状态 的传输协议,无状态是指协议对事务处理没有上下文的记忆能力,每一个请求都是完全独立的,但是我们中肯定有许多人并没有意识到 HTTP 协议无状态的重要性。

假如你做了一个简单的网页,其中包含了 1 个 HTML、2 个 Script 脚本、3 个 CSS、还有 10 张图片,这个网页成功展示在用户屏幕前,需要完成 16 次与服务端的交互来获取上述资源,由于网络传输各种等因素的影响,服务器发送的顺序与客户端请求的先后并没有必然的联系,按照可能出现的响应顺序,理论上最多会有 P(16,16) = 20,922,789,888,000 种可能性。试想一下,如果 HTTP 协议不是设计成无状态的,这 16 次请求每一个都有依赖关联,先调用哪一个、先返回哪一个,都会对结果产生影响的话,那协调工作会有多么复杂。

HTTP 协议的无状态特性又有悖于我们最常见的网络应用场景,典型就是认证授权,系统总得要获知用户身份才能提供合适的服务,因此,我们也希望 HTTP 能有一种手段,让服务器至少有办法能够区分出发送请求的用户是谁。为了实现这个目的,[RFC 6265] 规范定义了 HTTP 的状态管理机制,在 HTTP 协议中增加了 Set-Cookie 指令,该指令的含义是以键值对的方式向客户端发送一组信息,此信息将在此后一段时间内的每次 HTTP 请求中,以名为 Cookie 的 Header 附带着重新发回给服务端,以便服务端区分来自不同客户端的请求。一个典型的 Set-Cookie 指令如下所示:

Set-Cookie: jsessionid=8uP970gaErVN; Expires=Wed, 21 Feb 2022 07:28:00 GMT; Secure; HttpOnly

收到该指令以后,客户端再对同一个域的请求中就会自动附带有键值对信息jsessionid=8uP970gaErVN;,譬如以下代码所示:

GET /index.html HTTP/2.0
Host: baidu.com
Cookie: jsessionid=8uP970gaErVN;

根据每次请求传到服务端的 Cookie,服务器就能分辨出请求来自于哪一个用户。由于 Cookie 是放在请求头上的,属于额外的传输负担,不应该携带过多的内容,而且放在 Cookie 中传输也并不安全,容易被中间人窃取或被篡改。一般来说,系统会把状态信息保存在服务端,在 Cookie 里只传输的是一个无字面意义的、不重复的字符串,习惯上以sessionid或者jsessionid为名,服务器拿这个字符串为 Key,在内存中开辟一块空间,以 Key/Entity 的结构存储每一个在线用户的上下文状态,再辅以一些超时自动清理之类的管理措施。这种服务端的状态管理机制就是今天大家非常熟悉的 Session,Cookie-Session 也是最传统但今天依然广泛应用于大量系统中的,由服务端与客户端联动来完成的状态管理机制

Cookie-Session 方案在本章的主题“安全性”上其实是有一定先天优势的:状态信息都存储于服务器,只要依靠客户端的同源策略和 HTTPS 的传输层安全,保证 Cookie 中的键值不被窃取而出现被冒认身份的情况,就能完全规避掉上下文信息在传输过程中被泄漏和篡改的风险。Cookie-Session 方案的另一大优点是服务端有主动的状态管理能力,可根据自己的意愿随时修改、清除任意上下文信息,譬如很轻易就能实现强制某用户下线的这样功能。

Cookie-Session 分布式环境的局限

Session-Cookie 在单节点的单体服务环境中是最合适的方案,但当需要水平扩展服务能力,要部署集群时就开始面临麻烦了,由于 Session 存储在服务器的内存中,当服务器水平拓展成多节点时,设计者必须在以下三种方案中选择其一:

  • 牺牲集群的一致性(Consistency):让均衡器采用亲和式的负载均衡算法,譬如根据用户 IP 或者 Session 来分配节点,每一个特定用户发出的所有请求都一直被分配到其中某一个节点来提供服务,每个节点都不重复地保存着一部分用户的状态,如果这个节点崩溃了,里面的用户状态便完全丢失。
  • 牺牲集群的可用性(Availability):让各个节点之间采用复制式的 Session,每一个节点中的 Session 变动都会发送到组播地址的其他服务器上,这样某个节点崩溃了,不会中断都某个用户的服务,但 Session 之间组播复制的同步代价高昂,节点越多时,同步成本越高。
  • 牺牲集群的分区容忍性(Partition Tolerance):让普通的服务节点中不再保留状态,将上下文集中放在一个所有服务节点都能访问到的数据节点中进行存储。此时的矛盾是数据节点就成为了单点,一旦数据节点损坏或出现网络分区,整个集群都不再能提供服务。

只要在分布式系统中共享信息,CAP 就不可兼得,所以分布式环境中的状态管理一定会受到 CAP 的局限,无论怎样都不可能完美。但如果只是解决分布式下的认证授权问题,并顺带解决少量状态的问题,就不一定只能依靠共享信息去实现。

JWT(JSON Web Token)

Cookie-Session 机制在分布式环境下会遇到 CAP 不可兼得的问题,而在多方系统中,就更不可能谈什么 Session 层面的数据共享了,哪怕服务端之间能共享数据,客户端的 Cookie 也没法跨域。所以我们不得不重新捡起最初被抛弃的思路,当服务器存在多个,客户端只有一个时,把状态信息存储在客户端,每次随着请求发回服务器去。这样做的缺点是无法携带大量信息,而且有泄漏和篡改的安全风险。信息量受限的问题并没有太好的解决办法,但是要确保信息不被中间人篡改则还是可以实现的,JWT 便是这个问题的标准答案

JWT 令牌与 Cookie-Session 并不是完全对等的解决方案,它只用来处理认证授权问题,充其量能携带少量非敏感的信息,只是 Cookie-Session 在认证授权问题上的替代品,而不能说 JWT 要比 Cookie-Session 更加先进,更不可能全面取代 Cookie-Session 机制。

JWT(JSON Web Token)定义于[RFC 7519]标准之中,是目前广泛使用的一种令牌格式,尤其经常与 OAuth2 配合应用于分布式的、涉及多方的应用系统中。介绍 JWT 的具体构成之前,我们先来直观地看一下它是什么样子的,如图所示:

1655177123798.jpg

右边的 JSON 结构是 JWT 令牌中携带的信息,左边的字符串呈现了 JWT 令牌的本体。它最常见的使用方式是附在名为 Authorization 的 Header 发送给服务端,前缀在[RFC 6750]中被规定为 Bearer。如下代码展示了一次采用 JWT 令牌的 HTTP 实际请求:

GET /restful/products/1 HTTP/1.1
Host: baidu.com
Connection: keep-alive
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

右边的状态信息是对令牌使用 Base64URL 转码后得到的明文,请特别注意是明文,JWT 只解决防篡改的问题,并不解决防泄漏的问题,因此令牌默认是不加密的。尽管你自己要加密也并不难做到,接收时自行解密即可,但这样做其实没有太大意义。

JWT(JSON Web Token)三部分结构

从明文中可以看到 JWT 令牌是以 JSON 结构(毕竟名字就叫 JSON Web Token)存储的,结构总体上可划分为三个部分,每个部分间用点号.分隔开。

第一部分是 令牌头(Header),内容如下所示:

{
  "alg": "HS256",
  "typ": "JWT"
}

它描述了令牌的类型(统一为 typ:JWT)以及令牌签名的算法,示例中 HS256 为 HMAC SHA256 算法的缩写,其他各种系统支持的签名算法可以参考 jwt.io 网站所列。

散列消息认证码

哈希算法前出现“HMAC”的前缀,这是指散列消息认证码(Hash-based Message Authentication Code,HMAC)。可以简单将它理解为一种带有密钥的哈希摘要算法,实现形式上通常是把密钥以加盐方式混入,与内容一起做哈希摘要。

HMAC 哈希与普通哈希算法的差别是普通的哈希算法通过 Hash 函数结果易变性保证了原有内容未被篡改,HMAC 不仅保证了内容未被篡改过,还保证了该哈希确实是由密钥的持有人所生成的。

hmac.png

第二部分是 负载(Payload),这是令牌真正需要向服务端传递的信息。针对认证问题,负载至少应该包含能够告知服务端“这个用户是谁”的信息,针对授权问题,令牌至少应该包含能够告知服务端“这个用户拥有什么角色/权限”的信息。JWT 的负载部分是可以完全自定义的,根据具体要解决的问题不同,设计自己所需要的信息,只是总容量不能太大,毕竟要受到 HTTP Header 大小的限制。一个 JWT 负载的例子如下所示:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

JWT 在 RFC 7519 中推荐(非强制约束)了七项声明名称(Claim Name),如有需要用到这些内容,建议字段名与官方的保持一致:

  • iss(Issuer):签发人。
  • exp(Expiration Time):令牌过期时间。
  • sub(Subject):主题。
  • aud (Audience):令牌受众。
  • nbf (Not Before):令牌生效时间。
  • iat (Issued At):令牌签发时间。
  • jti (JWT ID):令牌编号。

第三部分是 签名(Signature),签名的意思是:使用在对象头中公开的特定签名算法,通过特定的密钥(Secret,由服务器进行保密,不能公开)对前面两部分内容进行加密计算,以例子里使用的 JWT 默认的 HMAC SHA256 算法为例,将通过以下公式产生签名值:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)

签名的意义在于确保负载中的信息是可信的、没有被篡改的,也没有在传输过程中丢失任何信息。因为被签名的内容哪怕发生了一个字节的变动,也会导致整个签名发生显著变化。此外,由于签名这件事情只能由认证授权服务器完成(只有它知道 Secret),任何人都无法在篡改后重新计算出合法的签名值,所以服务端才能够完全信任客户端传上来的 JWT 中的负载信息。

JWT 默认的签名算法 HMAC SHA256 是一种带密钥的哈希摘要算法,加密与验证过程均只能由中心化的授权服务来提供,所以这种方式一般只适合于授权服务与应用服务处于同一个进程中的单体应用。在多方系统或者授权服务与资源服务分离的分布式应用中,通常会采用非对称加密算法来进行签名,这时候除了授权服务端持有的可以用于签名的私钥外,还会对其他服务器公开一个公钥,公开方式一般遵循 JSON Web Key 规范。公钥不能用来签名,但是能被其他服务用于验证签名是否由私钥所签发的。这样其他服务器也能不依赖授权服务器、无须远程通信即可独立判断 JWT 令牌中的信息的真伪。

JWT(JSON Web Token) 存在的局限

JWT 令牌是多方系统中一种优秀的凭证载体,它不需要任何一个服务节点保留任何一点状态信息,就能够保障认证服务与用户之间的承诺是双方当时真实意图的体现,是准确、完整、不可篡改、且不可抵赖的。同时,由于 JWT 本身可以携带少量信息,这十分有利于 RESTful API 的设计,能够较容易地做成无状态服务,在做水平扩展时就不需要像前面 Cookie-Session 方案那样考虑如何部署的问题。现实中也确实有一些项目直接采用 JWT 来承载上下文来实现完全无状态的服务端,这能获得任意加入或移除服务节点的巨大便利,天然具有完美的水平扩缩能力。

尽管大型系统中只使用 JWT 来维护上下文状态,服务端完全不持有状态是不太现实的,不过将热点的服务单独抽离出来做成无状态,仍是一种有效提升系统吞吐能力的架构技巧。但是,JWT 也并非没有缺点的完美方案,它存在着以下几个经常被提及的缺点:

  • 令牌难以主动失效:JWT 令牌一旦签发,理论上就和认证服务器再没有什么瓜葛了,在到期之前就会始终有效,除非服务器部署额外的逻辑去处理失效问题,这对某些管理功能的实现是很不利的。譬如一种颇为常见的需求是:要求一个用户只能在一台设备上登录,在 B 设备登录后,之前已经登录过的 A 设备就应该自动退出。如果采用 JWT,就必须设计一个“黑名单”的额外的逻辑,用来把要主动失效的令牌集中存储起来,而无论这个黑名单是实现在 Session、Redis 或者数据库中,都会让服务退化成有状态服务,降低了 JWT 本身的价值,但黑名单在使用 JWT 时依然是很常见的做法,需要维护的黑名单一般是很小的状态量,许多场景中还是有存在价值的。
  • 相对更容易遭受重放攻击:首先说明 Cookie-Session 也是有重放攻击问题的,只是因为 Session 中的数据控制在服务端手上,应对重放攻击会相对主动一些。要在 JWT 层面解决重放攻击需要付出比较大的代价,无论是加入全局序列号(HTTPS 协议的思路)、Nonce 字符串(HTTP Digest 验证的思路)、挑战应答码(当下网银动态令牌的思路)、还是缩短令牌有效期强制频繁刷新令牌,在真正应用起来时都很麻烦。真要处理重放攻击,建议的解决方案是在信道层次(譬如启用 HTTPS)上解决,而不提倡在服务层次(譬如在令牌或接口其他参数上增加额外逻辑)上解决。
  • 只能携带相当有限的数据:HTTP 协议并没有强制约束 Header 的最大长度,但是,各种服务器、浏览器都会有自己的约束,譬如 Tomcat 就要求 Header 最大不超过 8KB,而在 Nginx 中则默认为 4KB,因此在令牌中存储过多的数据不仅耗费传输带宽,还有额外的出错风险。
  • 必须考虑令牌在客户端如何存储:严谨地说,这个并不是 JWT 的问题而是系统设计的问题。如果授权之后,操作完关掉浏览器就结束了,那把令牌放到内存里面,压根不考虑持久化才是最理想的方案。但并不是谁都能忍受一个网站关闭之后下次就一定强制要重新登录的。这样的话,想想客户端该把令牌存放到哪里?Cookie?localStorage?Indexed DB?它们都有泄漏的可能,而令牌一旦泄漏,别人就可以冒充用户的身份做任何事情。

2. 保密(Confidentiality)

保密是加密和解密的统称,是指以某种特殊的算法改变原有的信息数据,使得未授权的用户即使获得了已加密的信息,但因不知解密的方法,或者知晓解密的算法但缺少解密所需的必要信息,仍然无法了解数据的真实内容。按照需要保密信息所处的环节不同,可以划分为“信息在客户端时的保密”、“信息在传输时的保密”和“信息在服务端时的保密”三类。

保密的强度

保密是有成本的,追求越高的安全等级,就要付出越多的工作量与算力消耗。以用户登录为例,列举几种不同强度的保密手段,讨论它们的防御关注点与弱点:

  1. 以摘要代替明文:如果密码本身比较复杂,那一次简单的哈希摘要至少可以保证即使传输过程中有信息泄漏,也不会被逆推出原信息;即使密码在一个系统中泄漏了,也不至于威胁到其他系统的使用,但这种处理不能防止弱密码被[彩虹表攻击]所破解。
  2. 先加[盐值]再做哈希是应对弱密码的常用方法:盐值可以替弱密码建立一道防御屏障,一定程度上防御已有的彩虹表攻击,但并不能阻止加密结果被监听、窃取后,攻击者直接发送加密结果给服务端进行冒认。
  3. 将盐值变为动态值能有效防止冒认:如果每次密码向服务端传输时都掺入了动态的盐值,让每次加密的结果都不同,那即使传输给服务端的加密结果被窃取了,也不能冒用来进行另一次调用。尽管在双方通信均可能泄漏的前提下协商出只有通信双方才知道的保密信息是完全可行的(后续介绍“传输安全层”时会提到),但这样协商出盐值的过程将变得极为复杂,而且每次协商只保护一次操作,也难以阻止对其他服务的[重放攻击]。
  4. 给服务加入动态令牌,在网关或其他流量公共位置建立校验逻辑,服务端愿意付出在集群中分发令牌信息等代价的前提下,可以做到防止重放攻击,但是依然不能抵御传输过程中被嗅探而泄漏信息的问题。
  5. 启用 HTTPS 可以防御链路上的恶意嗅探,也能在通信层面解决了重放攻击的问题。但是依然有因客户端被攻破产生伪造根证书风险、有因服务端被攻破产生的证书泄漏而被中间人冒认的风险、有因[CRL]更新不及时或者[OCSP] Soft-fail 产生吊销证书被冒用的风险、有因 TLS 的版本过低或密码学套件选用不当产生加密强度不足的风险。
  6. 为了抵御上述风险,保密强度还要进一步提升,譬如银行会使用独立于客户端的存储证书的物理设备(俗称的 U 盾)来避免根证书被客户端中的恶意程序窃取伪造;大型网站涉及到账号、金钱等操作时,会使用双重验证开辟一条独立于网络的信息通道(如手机验证码、电子邮件)来显著提高冒认的难度;甚至一些关键企业(如国家电网)或机构(如军事机构)会专门建设遍布全国各地的与公网物理隔离的专用内部网络来保障通信安全。

客户端加密

客户端在用户登录、注册一类场景里是否需要对密码进行加密,这个问题一直存有争议。

为了保证信息不被黑客窃取而做客户端加密没有太多意义,对绝大多数的信息系统来说,启用 HTTPS 可以说是唯一的实际可行的方案。

为什么客户端加密对防御泄密会没有意义?原因是网络通信并非由发送方和接收方点对点进行的,客户端无法决定用户送出的信息能不能到达服务端,或者会经过怎样的路径到达服务端,在传输链路必定是不安全的假设前提下,无论客户端做什么防御措施,最终都会沦为“马其诺防线”。

中间人攻击(Man-in-the-Middle Attack,MitM)

通过劫持掉了客户端到服务端之间的某个节点,包括但不限于代理(通过 HTTP 代理返回赝品)、路由器(通过路由导向赝品)、DNS 服务(直接将你机器的 DNS 查询结果替换为赝品地址)等,来给你访问的页面或服务注入恶意的代码,极端情况下,甚至可能把要访问的服务或页面整个给取代掉,此时不论你在页面上设计了多么精巧严密的加密措施,都不会有保护作用。而攻击者只需地劫持路由器,或在局域网内其他机器释放 ARP 病毒便有可能做到这一点。

对于“不应把明文传递到服务端”的观点,也是有一些不同意见的。譬如其中一种保存明文密码的理由是为了便于客户端做动态加盐,因为这需要服务端存储了明文,或者某种盐值/密钥固定的加密结果,才能每次用新的盐值重新加密来与客户端传上来的加密结果进行比对。每次从服务端请求动态盐值,在客户端加盐传输的做法通常都得不偿失,客户端无论是否动态加盐,都不可能代替 HTTPS。真正防御性的密码加密存储确实应该在服务端中进行,但这是为了防御服务端被攻破而批量泄漏密码的风险,并不是为了增加传输过程的安全。

密码存储和验证

下面介绍一个普通安全强度的信息系统,密码如何从客户端传输到服务端,然后存储进数据库的全过程。

“普通安全强度”是指在具有一定保密安全性的同时,避免消耗过多的运算资源,验证起来也相对便捷。对多数信息系统来说,只要配合一定的密码规则约束,譬如密码要求长度、特殊字符等,再配合 HTTPS 传输,已足防御大多数风险了。

即使在用户采用了弱密码、客户端通信被监听、服务端被拖库、泄漏了存储的密文和盐值等问题同时发生,也能够最大限度避免用户明文密码被逆推出来。下面先介绍密码创建的过程:

客户端处理

  1. 用户在客户端注册,输入明文密码:123456
password = 123456
  1. 客户端对用户密码进行简单哈希摘要,可选的算法有 MD2/4/5、SHA1/256/512、BCrypt、PBKDF1/2,等等。为了突出“简单”的哈希摘要,这里笔者故意没有排除掉 MD 系这些已经有了高效碰撞手段的算法。
client_hash = MD5(password) // e10adc3949ba59abbe56e057f20f883e
  1. 为了防御彩虹表攻击应加盐处理,客户端加盐只取固定的字符串即可,如实在不安心,最多用伪动态的盐值(“伪动态”是指服务端不需要额外通信可以得到的信息,譬如由日期或用户名等自然变化的内容,加上固定字符串构成)。
client_hash = MD5(MD5(password) + salt)  // SALT = $2a$10$o5L.dWYEjZjaejOmN3x4Qu
  1. 假设攻击者截获了客户端发出的信息,得到了摘要结果和采用的盐值,那攻击者就可以枚举遍历所有 8 位字符以内(“8 位”只是举个例子,反正就是指弱密码,你如果拿 1024 位随机字符当密码用,加不加盐,彩虹表都跟你没什么关系)的弱密码,然后对每个密码再加盐计算,就得到一个针对固定盐值的对照彩虹表。为了应对这种暴力破解,并不提倡在盐值上做动态化,更理想的方式是引入慢哈希函数来解决。
client_hash = BCrypt(MD5(password) + salt)  // MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2

慢哈希函数 是指这个函数执行时间是可以调节的哈希函数,通常是以控制调用次数来实现的。BCrypt 算法就是一种典型的慢哈希函数,它做哈希计算时接受盐值 Salt 和执行成本 Cost 两个参数(代码层面 Cost 一般是混入在 Salt 中,譬如上面例子中的 Salt 就是混入了 10 轮运算的盐值,10 轮的意思是 210次哈希,Cost 参数是放在指数上的,最大取值就 31)。如果我们控制 BCrypt 的执行时间大概是 0.1 秒完成一次哈希计算的话,按照 1 秒生成 10 个哈希值的速度,算完所有的 10 位大小写字母和数字组成的弱密码大概需要 P(62,10)/(3600×24×365)/0.1=1,237,204,169 年时间。

服务端处理

  1. 只需防御被拖库后针对固定盐值的批量彩虹表攻击。具体做法是为每一个密码(指客户端传来的哈希值)产生一个随机的盐值。对于 Java 语言,从 Java SE 7 起提供了java.security.SecureRandom类,用于支持 CSPRNG 字符串生成。
SecureRandom random = new SecureRandom();
byte server_salt[] = new byte[36];
random.nextBytes(server_salt);   // tq2pdxrblkbgp8vt8kbdpmzdh1w8bex
  1. 将动态盐值混入客户端传来的哈希值再做一次哈希,产生出最终的密文,并和上一步随机生成的盐值一起写入到同一条数据库记录中。由于慢哈希算法占用大量处理器资源,但是Spring Security 5 中的BcryptPasswordEncoder,但是请注意它默认构造函数中的 Cost 参数值为-1,经转换后实际只进行了 210=1024 次计算,并不会对服务端造成太大的压力。此外,代码中并未显式传入 CSPRNG 生成的盐值,这是因为BCryptPasswordEncoder本身就会自动调用 CSPRNG 产生盐值,并将该盐值输出在结果的前 32 位之中,因此也无须专门在数据库中设计存储盐值字段。
server_hash = SHA256(client_hash + server_salt);  // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
DB.save(server_hash, server_salt);

以上加密存储的过程相对复杂,但是运算压力最大的过程(慢哈希)是在客户端完成的,对服务端压力很小,也不惧怕因网络通信被截获而导致明文密码泄漏。

密码存储后,以后验证的过程与加密是类似的,步骤如下:

  1. 客户端,用户在登录页面中输入密码明文:123456,经过与注册相同的加密过程,向服务端传输加密后的结果。
authentication_hash = MFfTW3uNI4eqhwDkG7HP9p2mzEUu/r2
  1. 服务端,接受到客户端传输上来的哈希值,从数据库中取出登录用户对应的密文和盐值,采用相同的哈希算法,对客户端传来的哈希值、服务端存储的盐值计算摘要结果。
result = SHA256(authentication_hash + server_salt);  // 55b4b5815c216cf80599990e781cd8974a1e384d49fbde7776d096e1dd436f67
  1. 服务端,比较上一步的结果和数据库储存的哈希值是否相同,如果相同那么密码正确,反之密码错误。
authentication = compare(result, server_hash) // yes

虽然此加密存储的过程安全性很高,但是遇到一些特殊需求,如需要服务端校验密码的格式(字母符号数字混合等),就会遇到困难,此类场景可以省略客户端加密步骤。

互联网架构安全性(一) 认证与授权篇
互联网架构安全性(二) 凭证与保密篇
互联网架构安全性(三) 传输与验证篇