Skip to main content

参考:应用的认证与授权

一份关于应用认证与授权的参考——session 与 JWT、撤销、带 PKCE 的 OAuth 2.0、在哪里强制执行什么,以及 RBAC、ABAC 与基于关系的授权模型。

它是什么

认证(authentication) 确立谁在调用;授权(authorization) 决定他们可以做什么。对一个应用来说,认证就是证明用户的身份(密码、联合登录),然后在之后的请求里以 session 或 token 的形式带上这个证明。授权就是把每个请求对照关于调用者和资源的规则进行检查。系统设计面试会问这两者各发生在哪里、一个 token 怎样不必每次请求都查数据库就能被验证,以及一个被偷走时会怎样。

Important

核心要点: 在边缘认证一次,在靠近数据的地方授权。 使用短生命周期的访问 token(加上可撤销的 refresh token),这样验证不需要查询,被盗时的损害也有限;身份要从已验证的 token 推出,绝不要从请求参数里取;并在拥有该资源的服务里强制执行授权。

何时需要它

每个 API 设计都有一个登录问题和一个权限问题,追问是 session 与 token 的对比、登出和撤销是怎么工作的,以及服务之间如何互相信任。

Note

面试信号: 说明无状态 token 在过期之前无法撤销,因此你会让它短命,并配一个可撤销的 refresh token,说明你知道”无状态”的代价。说”身份来自 token,绝不来自 user_id 参数”,可以预防一整类授权 bug。

Session 与 token

服务端 session签名 token(JWT)
状态服务器存储 session;客户端在 cookie 里持有一个不透明的 idtoken 自带 claim 和签名;服务器什么都不存
验证在 session 存储(一个缓存)里做查找检查签名和过期时间:不需要查询
撤销即时:删除 session困难:除非保留一个拒绝列表,否则在过期前一直有效
扩展需要共享的 session 存储无状态;任何服务器都能验证
大小很小的 id较大:每个请求都要带着
最适合单个域名下的浏览器应用;即时登出服务到服务和 API;跨服务调用

一个 JWT 由三部分组成,header.payload.signature。payload 里是 claim:谁(sub)、给谁用(aud)、谁签发(iss)、何时过期(exp),以及 scope。服务用签发者的公钥(或共享密钥)验证签名,检查 exp、iss 和 aud,然后信任这些 claim。签名证明 token 没有被篡改,但它不会隐藏 payload(它只是被编码了),所以不要把机密放进去。

撤销,无状态的代价。 让访问 token 短命(几分钟),并签发一个存储在服务端、可以被撤销的更长生命周期的 refresh token;客户端用它来换新的访问 token。登出和失陷通过撤销 refresh token 来处理,接受一个被偷的访问 token 在过期之前仍然有效。对高风险操作,检查一个撤销列表或要求重新认证。

OAuth 2.0 与 OpenID Connect

OAuth 2.0 让一个应用在不接触用户密码的情况下代表用户行事,方式是委托的、带范围的 token。OpenID Connect 在它之上加了一个身份层(一个说明用户是谁的 ID token),这就是”使用 … 登录”所用的。

Web 或移动应用的流程是带 PKCE 的授权码流程:

  1. 应用把用户重定向到授权服务器,并带上一个一次性的code challenge。
  2. 用户在那里认证,并对请求的 scope 表示同意。
  3. 服务器带着一个短命的授权码重定向回来。
  4. 应用用授权码(加上证明它发起了这个流程的 code verifier)换取一个访问 token和一个 refresh token。
  5. 应用用访问 token 调用 API。

PKCE 把授权码绑定到发起请求的应用,所以被截获的授权码毫无用处。每个公开客户端(单页应用和移动应用)都要用它;旧的 implicit 流程(在浏览器里返回 token)不应再使用。对没有用户的服务到服务调用,client credentials 流程给服务一个它自己的 token。

凭证用途
Session cookie浏览器用户;设置 HttpOnly、Secure 和 SameSite
访问 token(JWT 或不透明)代表用户或服务的一次 API 调用;短命
Refresh token获取新的访问 token;存在服务端、可撤销、轮换
API key第三方或脚本标识一个应用;只存哈希,支持轮换,限定范围,并做限流
mTLS 证书网络内的服务身份(通常由 service mesh 管理)

在哪里强制执行什么

  • 在边缘做认证。 网关(或 sidecar)验证一次 token,拒绝无效的,并通过一个受信任的 header 或一个内部 token,把已验证的身份传给各个服务。
  • 在拥有资源的服务里做授权。 只有拥有订单的服务知道谁可以读它。网关可以强制执行粗粒度的规则(scope、限流),但细粒度的检查属于数据所在的地方。
  • 身份来自上下文,绝不来自参数。 取自请求体的 user_id 会诱使人把 123 改成 124;要从已验证的 token 推出它。
  • 纵深防御。 内部服务之间仍然要互相认证(双向 TLS 或服务 token),因为一个扁平的受信任网络会让一个被攻破的服务够得到一切。

授权模型

模型思路适合弱点
RBAC(基于角色)用户有角色;角色有权限管理员、编辑、查看者角色爆炸;粒度粗
ABAC(基于属性)针对用户、资源和上下文的属性写规则(“部门匹配,且在工作时间内”)复杂的策略更难审计和测试
ReBAC(基于关系)权限沿着图里的关系传递(“一个文件夹的查看者可以查看其中的文档”)共享模型:文档、网盘、社交需要图存储,要留意延迟

基于关系的系统(如 Google 的 Zanzibar)存储 object#relation@user 元组,并以快速、一致的检查回答”用户 U 能对对象 O 做 R 吗?“;难点在于一致性(被撤销的权限必须及时生效)和在巨大规模下的性能。

密码与浏览器

  • 存储密码哈希,绝不存密码:一个慢速的、加盐的算法(bcrypt、scrypt 或 Argon2),每个用户一个盐。快速哈希(普通的 SHA-256)会让被偷走的表很快被破解。
  • 加上多因素认证、对登录尝试限流,并在反复失败时锁定或限速。
  • 浏览器保护:HttpOnly cookie(脚本读不到,限制了通过跨站脚本的窃取)、SameSite(限制跨站请求伪造)、Secure(仅 HTTPS)和内容安全策略。

Warning

生产实践: Token 是承载凭证(bearer credential):谁持有它,谁就是那个用户。要让生命周期短、只通过 TLS 发送、绝不记录日志、范围收窄,并按计划轮换签名密钥,由签发者发布当前的公钥。一个被偷走的长生命周期 token,是任何防火墙都看不到的一次入侵。

如何选择

情形选择原因
单个域名下的浏览器应用,需要即时登出cookie 里的服务端 session简单且可撤销
许多服务要不经查询地验证短命的签名 token,加 refresh token无状态验证
”使用 … 登录”或第三方访问带 PKCE 的 OAuth 2.0 与 OpenID Connect不共享密码,带范围的委托
服务到服务,没有用户Client credentials 或双向 TLS服务身份
第三方开发者API key(哈希存储、限定范围、轮换),或 OAuth标识并限制一个应用
资源共享(文档、文件夹)基于关系的授权权限沿关系传递
简单的管理员与用户角色RBAC容易推理

Caution

常见错误: 信任客户端提供的身份或角色(一个 user_id 参数、一个服务器并没有签发过的 isAdmin claim),或者只把授权放在网关里。要验证 token,从中取身份,并在拥有数据的服务里强制执行权限。

相关