文章目录
- OverView
- 凭证(Credentials)
- 1. 传统认证授权方式:Cookie-Session 机制
- 2. OAuth2 令牌概述
- 什么是 JWT
- JWT 令牌 结构
- Header
- Payload
- Signature
- JWT的优劣势
- 无状态架构的挑战
- 3. JWT 与 Cookie-Session 的对比
OverView
即使只限定在“软件架构设计”这个语境下,系统安全仍然是一个很大的话题。
接下来我们将对系统安全架构的各个方面进行详细分析,包括认证、授权、凭证、保密、传输安全和验证,结合案例实践,展示如何应用这些安全原则和技术,讨论具体解决方案和行业标准 ,并提供与业界标准相一致的解决方案。
计划:
-
认证(Authentication):
- 介绍认证的基本概念及其在软件架构中的作用。
- 讨论常见的认证方法(如用户名/密码、双因素认证、生物识别)及其实现方式。
- 探讨行业标准和最佳实践(如 OAuth、OpenID Connect)。
-
授权(Authorization):
- 定义授权的概念及其重要性。
- 讲解不同的授权模型(如基于角色的访问控制RBAC、基于属性的访问控制ABAC)。
- 介绍如何在架构中实现这些模型以及如何处理权限管理。
-
凭证(Credential):
- 阐明凭证的作用及其管理方式。
- 讨论如何确保证书和凭证的真实性、完整性和不可抵赖性。
- 介绍现有的凭证管理方案和技术(如 PKI、公钥基础设施)。
-
保密(Confidentiality):
- 解释数据保密的基本概念及其在系统中的应用。
- 讨论数据加密的技术和策略(如对称加密、非对称加密)。
- 介绍如何确保保密性,包括数据存储和处理中的加密措施。
-
传输(Transport Security):
- 定义传输安全及其对系统安全的影响。
- 讲解如何实现传输层安全(如 TLS/SSL)的具体方法。
- 讨论如何保护网络通信免受中间人攻击和数据篡改。
-
验证(Verification):
- 介绍数据验证的必要性及其对系统稳定性的影响。
- 讨论常见的验证技术(如输入验证、数据完整性检查)。
- 讲解如何在系统中实现数据验证机制以保证数据一致性和正确性。
凭证(Credentials)
1. 传统认证授权方式:Cookie-Session 机制
Cookie-Session 是传统 Web 应用中最常见的认证授权方式。
它的工作原理是:
- 服务端维护会话状态:当用户登录时,服务端生成一个会话 ID,并在服务端存储与该会话 ID 关联的用户数据。
- 客户端通过 Cookie 传递会话 ID:服务端将会话 ID 返回给客户端,客户端使用 Cookie 保存该 ID。每次请求时,客户端会将该 Cookie 发送给服务端,服务端通过该 ID 查找并恢复会话信息。
优点:
- 安全性高:由于会话数据存储在服务端,客户端只能通过会话 ID 访问数据,因此攻击者无法轻易获取和修改会话数据。
- 易于失效管理:服务端可以随时删除或更新会话,确保会话数据在需要时失效。
局限性:
Session-Cookie
在单节点的单体服务环境中是最合适的方案,但当需要水平扩展服务能力,要部署集群时就开始面临麻烦了,由于 Session 存储在服务器的内存中,当服务器水平拓展成多节点时,设计者必须在以下三种方案中选择其一:
牺牲集群的一致性(Consistency)
,让均衡器采用亲和式的负载均衡算法,譬如根据用户 IP 或者 Session 来分配节点,每一个特定用户发出的所有请求都一直被分配到其中某一个节点来提供服务,每个节点都不重复地保存着一部分用户的状态,如果这个节点崩溃了,里面的用户状态便完全丢失。牺牲集群的可用性(Availability)
,让各个节点之间采用复制式的 Session,每一个节点中的 Session 变动都会发送到组播地址的其他服务器上,这样某个节点崩溃了,不会中断对某个用户的服务,但 Session 之间组播复制的同步代价高昂,节点越多时,同步成本越高。牺牲集群的分区容忍性(Partition Tolerance)
,让普通的服务节点中不再保留状态,将上下文集中放在一个所有服务节点都能访问到的数据节点中进行存储。此时的矛盾是数据节点就成为了单点,一旦数据节点损坏或出现网络分区,整个集群都不再能提供服务。
在分布式系统中共享信息,CAP 就不可兼得,所以分布式环境中的状态管理一定会受到 CAP 的局限,无论怎样都不可能完美。但如果只是解决分布式下的认证授权问题,并顺带解决少量状态的问题,就不一定只能依靠共享信息去实现
言外之意是 JWT 令牌与 Cookie-Session 并不是完全对等的解决方案,它只用来处理认证授权问题,充其量能携带少量非敏感的信息,只是 Cookie-Session 在认证授权问题上的替代品,而不能说 JWT 要比 Cookie-Session 更加先进,更不可能全面取代 Cookie-Session 机制
2. OAuth2 令牌概述
OAuth2 中的访问令牌(Access Token)是授权服务器(Authorization Server)颁发给客户端应用(Client)的凭证,用于访问资源服务器(Resource Server)上的受保护资源。OAuth2 令牌通常有两种常见的形式:
-
JWT(JSON Web Token): 这是一种自包含的令牌,通常包含三部分:头部(Header)、负载(Payload)、签名(Signature)。JWT 是一种基于 JSON 的轻量级数据交换格式,它可以被签名和加密,确保数据的真实性和完整性。因为 JWT 是自包含的,所以资源服务器可以通过解码并验证签名来获取授权信息,而不需要额外查询授权服务器。
-
Opaque Token(不透明令牌): 这种令牌是一个随机生成的字符串,对客户端和资源服务器而言是不可读的。资源服务器需要将令牌发送回授权服务器进行验证和解码,以获取对应的授权信息。
什么是 JWT
官网: https://jwt.io
右边的 JSON 结构是 JWT 令牌中携带的信息,左边的字符串呈现了 JWT 令牌的本体。它最常见的使用方式是附在名为 Authorization
的 Header
发送给服务端,前缀在RFC 6750中被规定为 Bearer
看到 Authorization 这个 Header 与 Bearer 这个前缀时,便应意识到它是 HTTP 认证框架中的 OAuth 2 认证方案
如下代码展示了一次采用 JWT 令牌的 HTTP 实际请求:
GET /restful/products/1 HTTP/1.1
Host: artisan.cn
Connection: keep-alive
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpY3lmZW5peCIsInNjb3BlIjpbIkFMTCJdLCJleHAiOjE1ODQ5NDg5NDcsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwianRpIjoiOWQ3NzU4NmEtM2Y0Zi00Y2JiLTk5MjQtZmUyZjc3ZGZhMzNkIiwiY2xpZW50X2lkIjoiYm9va3N0b3JlX2Zyb250ZW5kIiwidXNlcm5hbWUiOiJpY3lmZW5peCJ9.539WMzbjv63wBtx4ytYYw_Fo1ECG_9vsgAn8bheflL8
上图右边的状态信息是对令牌使用 Base64URL 转码后得到的明文,请特别注意是明文,JWT 只解决防篡改的问题,并不解决防泄漏的问题,因此令牌默认是不加密的。尽管你自己要加密也并不难做到,接收时自行解密即可,但这样做其实没有太大意义.
JWT 令牌 结构
结构总体上可划分为三个部分,每个部分间用点号.分隔开。
Header
内容如下所示:
{
"alg": "HS256",
"typ": "JWT"
}
它描述了令牌的类型(统一为 typ:JWT)以及令牌签名的算法,示例中 HS256 为 HMAC SHA256 算法的缩写,其他各种系统支持的签名算法可以参考https://jwt.io/网站所列。
散列消息认证码
“HMAC”的前缀,这是指散列消息认证码(Hash-based Message Authentication Code,HMAC)。可以简单将它理解为一种带有密钥的哈希摘要算法,实现形式上通常是把密钥以加盐方式混入,与内容一起做哈希摘要。
HMAC 哈希与普通哈希算法的差别是普通的哈希算法通过 Hash 函数结果易变性保证了原有内容未被篡改,HMAC 不仅保证了内容未被篡改过,还保证了该哈希确实是由密钥的持有人所生成的。
Payload
JWT(JSON Web Token)的负载部分(Payload)是令牌的核心内容,包含了需要传递给服务端的各种信息。对于认证问题,负载至少应包含能够标识用户身份的信息;对于授权问题,负载则需要包含用户的角色或权限信息。由于 JWT 是通过 HTTP Header 传递的,负载的大小必须适中,以避免超出 HTTP Header 的大小限制(通常为 8 KB 左右)。
负载中的数据是经过 Base64URL 编码的,但未加密,因此虽然外界无法直接读取数据,但它仍然容易被解码和篡改,因此通常需要使用签名来确保数据的完整性和真实性。
JWT 负载的常见声明名称(Claim Name)
RFC 7519 推荐了七个常见的声明名称,虽然使用这些声明名称并非强制要求,但为了兼容性和标准化,建议在适用的情况下尽量采用这些字段。
-
iss(Issuer): 表示签发该令牌的实体(通常是授权服务器)。例如
"iss": "auth.mycompany.com"
。这个字段可以帮助服务端判断令牌是否来自可信来源。 -
exp(Expiration Time): 令牌的过期时间(UNIX 时间戳格式)。如
"exp": 1584948947"
。这是确保令牌不过期的一种方式,令牌过期后将不再有效。 -
sub(Subject): 令牌的主题,即令牌所代表的实体,通常是用户的唯一标识符。例如
"sub": "user123"
。这个字段用来标识该令牌是为谁生成的。 -
aud(Audience): 令牌的受众,表示令牌的接收者或目标应用程序。例如
"aud": "myapi.mycompany.com"
。确保令牌仅能被预期的应用程序使用。 -
nbf(Not Before): 表示令牌在指定时间之前是无效的(UNIX 时间戳格式)。例如
"nbf": 1584940000"
。这是防止令牌在过早使用的方式。 -
iat(Issued At): 令牌的签发时间(UNIX 时间戳格式)。例如
"iat": 1584938947"
。这有助于防止重放攻击,因为服务端可以使用这个时间戳检查令牌是否在合理的时间范围内。 -
jti(JWT ID): 令牌的唯一标识符,用于防止令牌的重复使用(防止重放攻击)。例如
"jti": "9d77586a-3f4f-4cbb-9924-fe2f77dfa33d"
。通常在一次性令牌中使用。
除了这些 RFC 7519 中定义的标准声明名称,在其他相关 RFC 文档和协议(如 OpenID Connect)中,还定义了一些具有特定用途的声明名称。这些公有声明通常用于特定的场景,如表示认证时间、认证上下文、认证方法等。
设计自定义 JWT 负载
在设计自定义 JWT 负载时,需要遵循以下原则:
-
选择合适的信息: 只包含业务逻辑需要的最小信息集。例如,对于一个电商应用,可能只需要存储用户 ID、角色、购物车 ID 等,而不需要包含用户的敏感信息如密码。
-
命名规范: 尽量使用标准化的字段名,以增强可读性和兼容性。若使用自定义字段,建议采用有意义的命名,并避免与标准字段冲突。
-
容量控制: 尽量减少负载的数据量,以确保 JWT 的总长度不会超过 HTTP Header 的大小限制。这可以通过删除冗余信息或使用简短的标识符来实现。
-
安全性考虑: 由于负载是可见的,敏感数据应避免直接存储在负载中。如果确实需要,可以使用加密方式保护敏感信息。此外,负载中的信息应根据需要进行签名,以防止被篡改。
示例负载:
{
"username": "artisan",
"authorities": [
"ROLE_USER",
"ROLE_ADMIN"
],
"scope": [
"ALL"
],
"exp": 1584948947,
"jti": "9d77586a-3f4f-4cbb-9924-fe2f77dfa33d",
"client_id": "bookstore_frontend"
}
在这个示例中,username
用于标识用户身份,authorities
和 scope
表示用户的权限范围,exp
指定令牌的过期时间,jti
是令牌的唯一标识符,而 client_id
表示该令牌是由哪个客户端应用请求的。
Signature
JWT 的第三部分是签名(Signature),它的主要作用是确保令牌的完整性和可信度。签名的核心在于对 JWT 的头部和负载部分进行加密计算,生成一个独特的哈希值。这个哈希值由特定的签名算法根据预先设定的密钥(Secret)生成。签名确保了 JWT 的内容没有被篡改,因为即使只有一个字节的改变,都会导致生成的签名发生显著变化。
对称加密签名(HMAC SHA256)
HMAC SHA256 算法:
HMAC SHA256 是一种基于密钥的哈希算法,用于生成加密签名。该算法将密钥与消息混合,生成一个固定长度的哈希值。具体而言,签名的生成过程如下:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)
其中,base64UrlEncode(header)
和 base64UrlEncode(payload)
分别是 JWT 头部和负载部分的 Base64URL 编码结果,secret
是一个由授权服务器保密的密钥。
单体应用场景:
HMAC SHA256 的优势在于其速度快、实现简单,但由于加密和解密都需要同一个密钥,因此在单体应用或授权服务与资源服务共存的系统中使用更为合适。在这种场景下,授权服务器与资源服务器可以共享同一个密钥,确保 JWT 的签名和验证可以在本地完成,而无需额外的通信开销。
- 非对称加密签名
非对称加密算法:
非对称加密算法(如 RSA、ECDSA)使用一对密钥来完成加密与解密:私钥用于生成签名,公钥用于验证签名。私钥必须严格保密,而公钥则可以公开给任何需要验证签名的实体。
分布式系统场景:
在分布式系统中,授权服务器和资源服务器往往分离,且处于不同的进程或物理服务器上。在这种情况下,使用非对称加密算法更为适合。授权服务器使用私钥对 JWT 进行签名,并将公钥公开给其他服务。任何收到 JWT 的服务都可以使用公钥验证签名的有效性,而无需与授权服务器通信,从而提高了系统的扩展性和性能。
公钥管理与 JWK:
JSON Web Key(JWK)是一个标准,用于描述公钥的信息。它通常以 JSON 格式提供,包含了公钥的类型、算法、用途等元数据。通过 JWK,其他服务可以方便地获取并使用公钥来验证 JWT 的签名。JWK 的管理和发布通常通过一个公开的 URL,授权服务器会定期更新并发布最新的公钥集合。
- 实践建议
算法选择:
- 小型系统:对于单体应用或小型系统,HMAC SHA256 是一种简单高效的选择。它的实现成本低,且在服务器与客户端共享密钥的情况下,能够快速完成签名验证。
- 分布式系统:对于大型、分布式系统,建议使用非对称加密算法(如 RSA 或 ECDSA)。这些算法支持公钥验证,使得系统中的不同服务可以独立进行 JWT 验证,从而减少对中心化授权服务器的依赖。
密钥管理:
- 密钥轮换:无论使用哪种算法,密钥的定期轮换都是必要的。对于对称加密,应定期更换密钥并确保新旧密钥在过渡期间都能够正常使用。对于非对称加密,应管理好私钥的安全性,同时及时更新并发布新的公钥。
- 密钥保护:私钥必须严格保密,尤其是在使用非对称加密的场景下。应避免私钥被泄露或盗取,因为这会导致 JWT 的安全性完全失效。
JWT的优劣势
- JWT 的优势
无状态设计和水平扩展能力:
JWT 在多方系统中表现出色的一大原因是其无状态设计。每个 JWT 都携带了足够的信息,使得服务端不需要维护额外的会话状态即可处理请求。这种无状态的特性使得系统能够轻松实现水平扩展。无论增加或移除服务节点,系统都不会受到影响,因为每个节点都可以独立验证和处理 JWT。
RESTful API 的天然适配性:
JWT 可以携带少量的上下文信息,非常适合 RESTful API 的设计。这种设计允许无状态的服务端处理请求,确保在服务重启或节点重置时,客户端可以无缝继续操作,而不需要重新建立会话状态。
分布式系统中的便捷性和可靠性:
在分布式系统中,JWT 的无状态特性极大地简化了跨服务的认证与授权流程。各个服务只需持有验证 JWT 签名的公钥,即可独立验证请求的有效性,避免了频繁的跨服务通信,减少了系统的耦合度。
- JWT 的主要缺点
难以主动失效的问题:
一旦 JWT 签发,在其到期前都会被认为是有效的。虽然这在无状态架构中是一个优点,但也带来了管理上的挑战。例如,在某些应用中,需要确保一个用户只能在一台设备上登录。这种场景下,采用 JWT 需要额外的逻辑处理,如黑名单机制,用来标记已失效的令牌。这种解决方案虽然有效,但也让系统部分退化为有状态服务,削弱了 JWT 原本的无状态优势。
容易遭受重放攻击:
JWT 的无状态特性使其容易成为重放攻击的目标。尽管这种攻击在任何基于令牌的认证系统中都是存在的,但由于 JWT 的广泛使用及其自身的无状态设计,使得这一问题更加突出。解决重放攻击的常见方法包括:
- HTTPS:在信道层面加密通信,防止令牌被截获和重放。
- 缩短令牌有效期:通过频繁刷新令牌,减少令牌被重放的时间窗口。
- 使用 Nonce 或序列号:通过在令牌中嵌入唯一的随机数或全局序列号,使每个请求独一无二,防止重复使用。
数据携带限制:
尽管 JWT 可以携带信息,但其容量是有限的,通常不应超过 4KB。过大的 JWT 会带来传输效率问题,并且可能超出某些服务器或浏览器的 Header 限制。这限制了 JWT 在某些场景下的使用,开发者需要谨慎设计其负载内容,避免在 JWT 中携带过多的数据。
客户端存储的安全性:
JWT 需要在客户端安全存储。理想情况下,JWT 应只存储在内存中,但这意味着用户每次关闭浏览器后都需要重新登录。如果将 JWT 存储在持久化存储中(如 Cookie、localStorage 或 Indexed DB),则面临被泄露的风险。为减小这一风险,建议:
- 使用 Secure Cookie:将 JWT 存储在 HTTP-only、Secure 标记的 Cookie 中,减少 XSS 攻击风险。
- 限制令牌的权限与有效期:即使令牌被泄露,也能最大程度地减少其危害。
无状态架构的挑战
在线用户实时统计功能:
无状态架构虽然有许多优点,但在某些场景下却显得不够灵活。比如,要在一个完全无状态的架构中实现在线用户的实时统计是一件非常困难的事情。由于服务端不维护用户状态,无法直接统计当前活跃的用户数。可能的解决方案包括:
- 利用心跳机制或定期请求:客户端定期发送请求,服务端根据这些请求推算在线用户数,但这会带来额外的网络和计算开销。
- 混合架构:在特定功能上引入有状态的设计,结合使用 JWT 和 Session,既能保持大部分服务的无状态特性,又能满足特定场景下的状态需求。
3. JWT 与 Cookie-Session 的对比
客户端状态存储(JWT) vs 服务端状态存储(Cookie-Session):
-
状态存储位置:
- JWT:状态存储在客户端,通过令牌本身包含所有授权信息。优点是可以减少服务器的状态管理负担,适合分布式系统中的无状态服务。
- Cookie-Session:状态存储在服务端,客户端仅持有一个指向服务端状态的会话 ID。优点是数据集中管理,便于控制和更新。
-
安全性:
- JWT:虽然传输中可以加密,但一旦泄露,攻击者即可获取和使用全部授权信息。由于是无状态的,强制失效困难。
- Cookie-Session:因为数据存储在服务端,攻击者无法通过会话 ID 窃取用户数据。强制失效也更为直接。
-
分布式系统中的应用:
- JWT:更适合分布式系统和微服务架构,因为它不需要在服务间共享会话数据。
- Cookie-Session:对于集中式架构或少量服务的系统更为适合,但在大规模分布式系统中可能带来扩展性和性能问题。