Dawn's Blogs

分享技术 记录成长

0%

凤凰架构笔记 (5) 安全之认证和凭证

认证

认证就是识别用户的身份,通常而言有两种方式进行认证,一是 HTTP 认证(在请求资源时认证),二是Web 内容认证(在获取服务时认证)。

HTTP 认证

HTTP 认证通过 Authication Header 实现,服务器告知客户端应该采用哪种认证方式:

1
2
WWW-Authenticate: <认证方案> realm=<安全区域>
Proxy-Authenticate: <认证方案> realm=<安全区域>

客户端进行响应:

1
2
Authorization: <认证方案> <凭证内容>
Proxy-Authorization: <认证方案> <凭证内容>

认证方案如下:

  • Basic:让用户输入用户名和密码,经过 Base64 编码后作为凭证内容。
  • Digest:HTTP 摘要认证,Digest 认证把用户名和密码加盐(Nonce)后通过哈希后发送出去。
  • Bearer:基于 OAuth 2 规范来完成认证。

Web 认证

Web 认证就是通过 Web 表单让用户去填写用户名密码等信息。

这种认证方法没有固定的标准,没有一个统一的标准去规定用户名、密码、验证码是否需要加密,并且用何种方法进行加密。这不是缺点,反而是一大优点,因为足够的自主性,可以设计出符合需求的各种认证方式。

请注意,Web 认证是可以基于 HTTP 认证实现的,也就是说可以使用 HTTP Authentication Header 记录认证信息。

WebAuthn

2019 年 W3C 起草了第一份 Web 内容认证标准 WebAuthn,WebAuthn 彻底抛弃了传统的密码登录方式,改为直接采用生物识别(指纹、人脸、虹膜、声纹)或者实体密钥(以 USB、蓝牙、NFC 连接的物理密钥容器)来作为身份凭证。所以,这个规范不关注界面该是什么样子、要不要验证码、是否要前端校验这些问题。

WebAuthn包含注册和登录两大流程。

注册

注册的流程为:

  1. 用户进入系统的注册页面,这个页面的格式、内容和用户注册时需要填写的信息均不包含在 WebAuthn 标准的定义范围内。
  2. 当用户填写完信息,点击提交后,服务端先暂存用户提交的数据,生成一个随机字符串(规范中称为 Challenge)和 User ID 返回给客户端
  3. 客户端的 WebAuthn API 将 Challenge 和 User ID,把这些信息发送给验证器,验证器可理解为用户设备上 TouchID、FaceID、实体密钥等认证设备的统一接口。
  4. 验证器提示用户进行验证,产生一个密钥对,由验证器保存私钥、用户信息、服务器域名。然后使用私钥对 Challenge 进行签名,将签名结果、UserID 和公钥一起返回客户端。
  5. 浏览器将验证器返回的结果转发给服务器。
  6. 服务器核验信息,检查 UserID 与之前发送的是否一致,并用公钥解密后得到的结果与之前发送的 Challenge 相比较,一致即表明注册通过,由服务端存储该 UserID 对应的公钥

image-20230608212857650

登录

登录过程与注册过程类似,流程如下:

  • 用户访问登录页面,填入用户名后即可点击登录按钮。
  • 服务器返回 Challenge、User ID。
  • 浏览器将 Challenge 和 User ID 发送给验证器进行验证。
  • 验证器通过私钥加密 Challenge,返回给浏览器。
  • 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。

凭证

服务器认证完成后,会返回给客户端一个凭证(token,访问令牌)。客户端之后的请求都会携带这个 token 作为凭证。

关于认证状态存储在哪里,有两种解决方案,一种是存储在服务器(Cookie-Session),一种是存储在客户端(JWT)。

通过在 Cookie 中携带 Session ID,服务器以 Session ID 为 key存储用户状态上下文信息。通过这种方式,可以很容易的实现强制用户下线的功能,同时通过同源策略和 HTTPS 可以避免上下文信息被篡改和泄漏的情况。

但是在集群部署时,Cookie-Session 就发生了问题,由于 Session 存储在服务器中,设计者必须选择以下方案:

  • 牺牲一致性:通过负载均衡算法,使得根据特定用户访问到同一个节点上,如果这个节点崩溃则用户信息丢失。
  • 牺牲可用性:在集群节点之间同步复制 Session 信息,这种同步通常代价很高。
  • 牺牲分区容错性:将 Session 信息保存到所有服务器节点都能访问到的数据节点中存储,但是数据节点会成为单点,一旦宕机整个集群不可用。

JWT

JWT(JSON Web Token)是一种广泛使用的令牌格式,它分为三个部分:令牌头、负载、签名。这三个部分都用 base64 编码,用 . 分隔开。

注意,令牌头和负载是明文传输的,签名解决篡改的问题,默认是不加密的。

JWT 使客户端保存用户状态信息,有效的解决的服务集群部署的问题。但是它有几个缺陷:

  • 难以主动失效:token 一旦签发就交由客户端保管了,在到期之前始终有效。如果实现主动失效的逻辑,就必须在服务端设计一个黑名单的逻辑,把要主动失效的令牌集中存储起来(这会让服务器退化为有状态,但是维护的数据量很小,也是常用的设计方法)。
  • 携带额外的数据:HTTP 协议并没有强制约束 Header 的最大长度,但是各种浏览器、服务器都有限制。在令牌中存储过多的数据不仅耗费传输带宽,还有额外的出错风险。

令牌头

第一部分是令牌头(Header),它记录了签名算法类型(统一为 JWT)。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

负载

第二部分为负载(Payload),这是令牌真正需要向服务端传递的信息。WT 在 RFC 7519 中推荐了七项声明名称(Claim Name):

  • iss(Issuer):签发人。
  • exp(Expiration Time):令牌过期时间。
  • sub(Subject):主题。
  • aud (Audience):令牌受众。
  • nbf (Not Before):令牌生效时间。
  • iat (Issued At):令牌签发时间。
  • jti (JWT ID):令牌编号。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"username": "icyfenix",
"authorities": [
"ROLE_USER",
"ROLE_ADMIN"
],
"scope": [
"ALL"
],
"exp": 1584948947,
"jti": "9d77586a-3f4f-4cbb-9924-fe2f77dfa33d",
"client_id": "bookstore_frontend"
}

签名

第三部分是签名,签名保证了 header 和 payload 没有被篡改。通过以下公式得到,密钥 Secret 保证了签发人的身份(只有它知道密钥)。

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

可以采用非对称加密算法进行签名,这时候除了授权服务端持有的可以用于签名的私钥外,还会对其他服务器公开一个公钥。公钥不能用来签名,但是能被其他服务用于验证签名是否由私钥所签发的。这样其他服务器也能不依赖授权服务器、无须远程通信即可独立判断 JWT 令牌中的信息的真伪。