架构安全性设计、部分示例及原理分析

 

1 为什么写?

个人最近看了周志明的《凤凰架构》中架构安全性部分,书中对于架构安全性做了非常体系的讲解,开拓了自己的视野,希望通过本文能够对其中的关键点做下实战和总结。 如今,大家也比较关心个人的隐私问题,学好安全性相关的内容,能够对于安全性相关的设计提供一些理论指导,能够明白安全性做得好的公司为什么要这么设计,也能指导自己在公司设计出更加安全的架构,而安全相关问题基本上是与具体业务无关,会有通用的标准,掌握之后可以应用在任何业务场合。

2 你能收获什么内容?

  1. 架构安全性包含的部分有哪几类,以及每一类的侧重点是什么?大家也可以通过周志明的文章进一步了解细节。
  2. 对于每一个类中的安全性比较主流/倾向的做法是什么?这些做法的原理是什么?并且能够通过一些示例分析加深理解。

3 架构安全性包含的内容及本文讲解的关键技术点

架构安全性包含比较多的内容,其中至少包含了以下六个部分。 以下定义扩充自周志明书中的定义:

认证(Authentication):系统如何正确分辨出操作用户的真实身份?(需要解决你是谁的问题)
授权( Authorization):系统如何控制一个用户该看到哪些数据、能操作哪些功能?(需要解决你能干什么的问题)
凭证(Credential):系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的?
保密(Confidentiality):系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用?
传输(Transport Security):系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充?
验证(Verification):系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险?  

o_220129033442_%E6%9E%B6%E6%9E%84%E6%B5%81%E7%A8%8B%E5%9B%BE.png

3.1 认证

以HTTP协议为基础的认证框架,一般是依靠内容而不是传输协议来实现的认证方式,由于实现形式上一般是用了登录表单的方式,因此通常也被称为“表单认证”。 在2019年之前,表单认证没有什么行业标准可循,表单什么样子,其中的用户字段、密码字段、验证码字段是否要在客户端加密、采用何种方式加密,接受表单的服务地址是什么?都完全由客户端和服务端的开发者自行协商决定。 直到2019年3月,万维网联盟批准了一个世界首份Web内容标准“WebAuthn”,WebAuthn彻底抛弃了传统的密码登录方式,改为直接采用生物识别(指纹、人脸等)或者实体密钥来作为身份凭证,体验和安全性上较既有的表单认证方式有较好的提升。 一些APP客户端已经有启用了这些认证方式,chrome的最新版本也提供了webAuthn的支持,但是到目前为止,已经支持webAuthn登录的网站还比较少,以下是一个网友实现的一个Demo,登录的效果如下:

o_220129094431_zhiwendenglu.gif

实现的代码可以从网友的原始博文中获取。

使用webAuthn注册和验证过程原理比较类似,下图展示了注册流程:

o_220129120953_webAuthn%E6%B5%81%E7%A8%8B.png

图里的注册流程摘自网站,解释如下(暂时看不懂没有关系,你只要知道它作为认证非常安全,体验也很好):

0.应用程序请求注册 - 应用程序发出注册请求。这个请求的协议和格式不在 WebAuthn 标准的范围内。
1.服务器发送挑战、用户信息和依赖方信息 - 服务器将挑战、用户信息和依赖方信息发送回应用程序。在这里,协议和格式不在 WebAuthn 标准的范围内。通常,这可以是基于 HTTPS 连接的 REST(可能会使用 XMLHttpRequest 或 Fetch)API。不过只要在安全连接中,也可以使用 SOAP、RFC 2549 或几乎任何其他协议。从服务器接收到的参数将传递给 create() ,大部分情况下只需很少修改甚至不需要做任何修改。create() 会返回一个Promise,并返回包含 AuthenticatorAttestationResponse (en-US) 的 PublicKeyCredential (en-US)。需要注意的是挑战必须是随机的 buffer(至少 16 字节),并且必须在服务器上生成以确保安全。
2.浏览器向认证器调用 authenticatorMakeCredential() - 在浏览器内部,浏览器将验证参数并用默认值补全缺少的参数,然后这些参数会变为 AuthenticatorResponse.clientDataJSON。其中最重要的参数之一是 origin,它是 clientData 的一部分,同时服务器将能在稍后验证它。调用 create() 的参数与clientDataJSON 的 SHA-256 哈希一起传递到身份验证器(只有哈希被发送是因为与认证器的连接可能是低带宽的 NFC 或蓝牙连接,之后认证器只需对哈希签名以确保它不会被篡改)。
3.认证器创建新的密钥对和证明 - 在进行下一步之前,认证器通常会以某种形式要求用户确认,如输入 PIN,使用指纹,进行虹膜扫描等,以证明用户在场并同意注册。之后,认证器将创建一个新的非对称密钥对,并安全地存储私钥以供将来验证使用。公钥则将成为证明的一部分,被在制作过程中烧录于认证器内的私钥进行签名。这个私钥会具有可以被验证的证书链。
4.认证器将数据返回浏览器 - 新的公钥、全局唯一的凭证 ID 和其他的证明数据会被返回到浏览器,成为 attestationObject。
5.浏览器生成最终的数据,应用程序将响应发送到服务器 - create() 的 Promise 会返回一个 PublicKeyCredential (en-US),其中包含全局唯一的证书 ID PublicKeyCredential.rawId (en-US)  和包含 AuthenticatorResponse.clientDataJSON 的响应 AuthenticatorAttestationResponse (en-US)。你可以使用任何你喜欢的格式和协议将 PublicKeyCredential (en-US) 发送回服务器(注意 ArrayBuffer 类型的属性需要使用 base64 或类似编码方式进行编码)
6.服务器验证数据并完成注册 - 最后,服务器需要执行一系列检查以确保注册完成且数据未被篡改。步骤包括:
  - 验证接收到的挑战与发送的挑战相同
  - 确保 origin 与预期的一致
  - 使用对应认证器型号的证书链验证 clientDataHash 的签名和证明
  验证步骤的完整列表可以在 WebAuthn 规范中找到。一旦验证成功,服务器将会把新的公钥与用户帐户相关联以供将来用户希望使用公钥进行身份验证时使用。   

WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能。也解决了传统密码在网络传输上的风险。 当前的 WebAuthn 还很年轻,普及率暂时还很有限,但书中作者相信几年之内它必定会发展成 Web 认证的主流方式,被大多数网站和系统所支持。

3.2 授权

授权实质上是在解决:“谁(User)能够操作(Operation)哪些资源(Resource)”。日常开发中比较多见的是RBAC和OAuth2两种访问控制和授权方案。 RBAC通过引入角色概念,赋予角色操作资源的权限,并且给用户赋予权限能够让拥有这些角色的用户操作对应的资源。基本上公司的权限控制模块都是基于RBAC实现的。

而OAuth2 是面向于解决第三方应用的认证授权协议。允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 比如很多网站都有通过微信扫一扫登录的功能,也是使用了OAuth2的认证授权过程。

微信扫一扫登录使用了OAuth2中授权码模式(其他的更多模式可以参考《凤凰架构》),OAuth2的授权码方式的访问流程如下图所示:

o_220212083558_%E6%8E%88%E6%9D%83%E7%A0%81code.png

在微信扫一扫登录场景中,图中的资源所有者就是用户,操作代理是浏览器,第三方应用是希望使用微信扫一扫登录的第三方网站,授权服务器是微信服务端。

比如简书使用微信扫一扫登录的过程如下:

  • 选择微信扫一扫登录,打开该服务器的微信的二维码认证页面,该页面带有原始网页的回调地址:

    https://open.weixin.qq.com/connect/qrconnect?appid=wxe9199d568fe57fdd&client_id=wxe9199d568fe57fdd&redirect_uri=https%3A%2F%2Fwww.jianshu.com%2Fusers%2Fauth%2Fwechat%2Fcallback&response_type=code&scope=snsapi_login&state=%257B%257D#wechat_redirect      
    
  • 用户扫描微信,认证成功后返回code,如下图所示:

    o_220212082923_%E7%AE%80%E4%B9%A6callback.png

  • 第三方应用服务器使用code,appsecret获取accesstoken 并且拿到微信用户的基本信息,标记用户已经登录。
  • 重定向到第三方登录成功后的页面。

3.3 凭证

HTTP 协议是一种无状态的传输协议,每一个请求都是完全独立的,所以一般web开发都会采用cookie-session机制让服务器有办法能够区分出发送请求的用户是谁,在服务器中会维护了一些用户的session信息,为了服务器的高可用,会把用户的状态信息存储到Redis等集中式的存储中。 除了cookie-session机制,还可以使用JWT(JSON Web Token)的方案,能够让信息不存储在服务端,且也能防止信息在传输过程中被篡改。

JWT的原理:服务器认证以后,生成JSON 对象,发回给用户。以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。 JWT:的数据结构由三部分组成Header(头部)、Payload(负载)、Signature(签名)。写成一行,就是下面的样子:

Header.Payload.Signature

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

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

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据,可以定义业务需要的私有字段,JWT 规定了7个官方字段,供选用。

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

Signature 部分是对前两部分的签名,防止数据篡改。需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,其中header和payload部分需要使用Base64URL进行转化。然后三个部分之间用"点"(.)分隔,就可以返回给用户。

最后的效果如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwMTIxMiIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.RoeGlqmkqtGrgjmV0Z5EF8bwCLQdzRXwPiG1ZmiNVfU   

可以通过网站https://jwt.io/, 查看和生成编码后的数据。java有jjwt工具可以在项目中生成和解析JWT, github地址:https://github.com/jwtk/jjwt

客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

JWT的应用目前感觉还是比较少,但是提供了一种服务端无状态的思路,正常情况下使用cookie-session机制能够满足要求了。

3.4 保密

密码如何从客户端传输到服务端,一般会在客户端对用户密码进行简单的哈希摘要。从根本上杜绝在服务器数据库或者日志中存储密码明文, 大网站被拖库的事情层出不穷,如果使用明文,就比较危险。 但是不存储明文,如果只是做了简单的哈希在服务端进行存储,也比较容易通过彩虹表攻击得到明文,为了应对彩虹表攻击应加盐处理。 下面会重点讲下为什么普通的hash方式拿到密文后容易被破解。

比如windows xp的登录密码会通过一些hash算法保存在sam文件,通过彩虹表攻击可以快速的获取到windows xp的登录密码,如下是通过应用了彩虹表的ophcrack工具破解后的截图:

o_220212063159_Ophcrack.png

案例中包含了字母和数字的14长度的密码在15分钟内被破解,如果更简单的密码,可以使用轻量级的彩虹表,基本上在秒级就可以被破解掉。

以14位字母和数字的组合密码为例,如果使用穷举法进行破解,共有1.24×1025((10+26+26)14)种可能,即使电脑每秒钟能进行10亿次运算,也需要4亿年才能破解。 如果使用海量的磁盘进行hash串的存储,然后通过查表法获取明文,就算只有128位的哈希串的hash串存储(先不考虑明文存储)就是一个天文数字(2128/8 字节)。

那么彩虹表的做法是在计算时间和存储空间中做了下平衡。

彩虹表会由很多条链组成,针对每一条链,会有很多链的节点,这些链节点之间的关系如下:

【第一个节点(明文1)】->(H)-->【第二个节点(Hash值1)】-->(R1)--->【第三个节点(明文2)】-->(H)-->【第四个节点(Hash值2)】-->(R2)-->【第五个节点(明文3)】

示例:
12345->(H)->abwefsdfse->(R1)->32112->(H)->asdasgasdf->(R2)-->13423

- 其中H是要破解Hash串使用的Hash函数。
- R1~Rx是彩虹表构建的能够从Hash串到明文的函数。

而彩虹表存储的时候只需要存储这条链的第一个节点和最后一个节点,从而大大减少了存储空间(链条越长,节点个数越多,节约空间越长)。

彩虹表破解过程如下:

假如一条链上有K个Hash值的节点
1. 假设要破解的密文位于某一链条的最后一个Hash值节点位置处,先对其进行Rk运算,看是否能够在末节点中找到对应的值。如果找到,生成这个Hash值的明文即为要破解的明文。
2. 如果找不到,继续判断要破解的密文位于倒数第二个Hash值节点位置处,那么对其做R(k-1),H,RK三步操作,然后再进行末节点的比对。
3. 以此类推,最终找到要破解的明文。

彩虹表自己构建比较麻烦,可以直接从互联网下载各种Hash方式和大小的彩虹表。 说了这个彩虹表的例子,是说我们需要在做保密的时候增强对于彩虹表破解的难度,可以在Hash的时候加上盐值。

3.5 传输

传输层非常重要,如果传输层是明文传递,比如使用了HTTP传输,那么非常容易被第三方获取到http内容。 为了让http传输不被第三方窃取,通过https是唯一的手段。那么https是怎么确保安全的呢? 简单来说HTTPS使用了对称加密和非对称加密,先通过非对称加密传递了对称加密密钥,然后后续通过对称加密传递http内容,非对称加密中的私钥由服务端保存,而公钥可以公开给所有人,通过公钥加密的内容只能由服务端的私钥解密获得。

目前HTTPS一般采用的是TLS1.2协议,TSL的握手时序如下图所示:

o_220212091029_TLS%E6%8F%A1%E6%89%8B%E8%BF%87%E7%A8%8B.png

在第二步服务端返回Server Hello的同时,也会返回包含了非对称加密公钥的数字证书。 客户端就使用该公钥对对称加密的密钥进行加密再传递给服务端。

通过WireShark抓包也可以清楚看到整个握手过程,如下图所示:

o_220211105723_https%E4%BC%9A%E8%AF%9D%E8%BF%87%E7%A8%8B.png

至此,还有一个问题,就是客户端怎么确保传递给他的公钥就是服务端希望它拿到的公钥呢。这边就需要使用到了CA机构。CA机构需要对包含了服务器公钥的数字证书进行签名。 然后客户端会在操作系统中内置了一些CA根证书,用来对数字证书的有效性进行验证,从而确保客户端拿到正确的公钥。

出于好奇,我通过互联网了解了下是不是存在CA机构泄漏私钥的情况,如果CA机构的私钥泄露了,那就是一件非常危险的事情,那么基于该CA机构签名的数字证书就会不可信了。 历史上确实存在CA机构泄漏私钥的情况,比如荷兰的CA安全证书提供商DigiNotar,服务器遭受到了黑客入侵,私钥被窃取。攻击者基于此私钥共发行了 531 个伪造证书,然后微软紧急发布了操作系统补丁, 将其列入不信任CA名单,而DigiNotar也因此宣告破产。

而我们平时如果希望在客户端上抓HTTPS包进行分析,可以用两类方式:

  1. 使用抓包工具签发证书,并且在操作系统中标记签发证书的机构是受信的。比如charles就是用这个方式进行https的抓包。
  2. 获取到https对称加密的密钥,并且使用该密钥对获取的加密内容进行解密。比如wireshark使用chrome https传输的对称密钥。

3.6 验证

这部分更多的提到了业务验证,平时我们实际写业务代码最多的也是这个部分,往往会在从客户端到接入层到逻辑层的各层进行验证。 代码中进行业务逻辑验证一般会存在的两个问题:

  1. 代码逻辑里充斥着大量的判空逻辑及其他校验,影响代码的简洁
  2. 调用端和被调用端在哪层做校验更加合理

书中作者提倡的做法是把校验行为从分层中剥离出来,不是具体在哪一层做逻辑校验,而是在 Bean 上做。即 Java Bean Validation。 而在Bean上做的可以比较容易地在各层做到重用。比如以下面的代码为例,对于Account的验证可以通过@UniqueAccount进行标注就可以。

public Response createUser(@Valid @UniqueAccount Account user) {
  return CommonResponse.op(() -> service.createAccount(user));
}

更多例子和使用方式,可以通过周老师的凤凰架构查看。

4 总结

感谢周老师的书籍(文中有部分截图也引用自周老师的书籍网站),让我更加体系化地进行了安全部分的学习,并且通过一些示例加深了对于该部分的理解,同时也希望能够帮助大家理解。如果大家希望更完整地了解相关内容,建议也完整地读一下周老师的书籍。欢迎大家评论交流。

5 附录

个人也开通了微信公众号,大家可以关注公众号参与评论,也及时收到最新的文章推送。

o_220212151121_cs%20%E5%85%AC%E4%BC%97%E5%8F%B7.jpg

Author: csophys

Created: 2022-02-12 Sat 23:46

Validate

posted @ 2022-02-12 23:46  csophys  阅读(812)  评论(0编辑  收藏  举报