Skip to content
This page has been auto-translated and may contain errors.View in English

身份验证与授权

用户发出的每一个请求都会带出两个问题,而且这两个问题是按顺序回答的。

这是谁? 以及 他们能做什么?

第一个问题是身份验证(authentication),第二个是授权(authorization)。两者都常被简称为"auth",开头六个字母也一模一样,但它们解决的是完全不同的问题。

证明你是谁

身份验证只回答一个问题:眼前这个人真的是他自称的那个人吗?

作为用户,你已经接触过常见的验证方式:

  • 用户名加密码。
  • 用 Google 或 GitHub 账号登录。
  • 在请求中附带 API key 或 token。

每一种方式都是在出示某种"证据",服务器的工作就是判断这份证据是否站得住脚。

一旦这一步出错,别人就能冒充成另一个人。这就是仿冒(spoofing),也就是 STRIDE 里的 S,也是那种一旦发生、其他所有防护都会失去意义的错误——因为应用现在是把正确的规则用在了错误的人身上。

Juno证明你是谁 可以把身份验证想象成一道门,门之后才是你进去之后能碰哪些东西的问题。

这道门并不关心你想做什么,它唯一的工作就是判断你是不是你自称的那个人。

Juno证明你是谁 状态码的用法值得弄清楚,因为它们的名字其实很容易误导人。401 叫 Unauthorized,但实际意思是"未认证":我们不知道你是谁,所以先登录。403 叫 Forbidden,意思是我们清楚地知道你是谁,但你依然不能做这件事。

如果用户根本没有会话(session)时你返回 403,就等于告诉客户端"放弃吧",而正确的做法本应是提示用户登录。

Juno证明你是谁 要记住的关键区别是:身份验证每个会话只发生一次,产出的是一个"声明";而授权在每次请求时都会发生,消费的正是这个声明。大多数身份相关的 bug,本质上都是这个声明在其背后的事实发生变化之后,还被继续信任了多久的问题。

还有第三个经常被混进来的词,值得单独拎出来说清楚。"标识(identification)"是提出一个声明,"身份验证(authentication)"是证明这个声明,而"可问责性(accountability)"是事后能说清楚谁做了什么。

STRIDE 之所以给"抵赖(repudiation)"单独分配了一个字母,正是因为一个系统完全可能正确完成了身份验证,事后却依然无法证明是谁执行了某个操作。

决定你能做什么

授权发生在身份验证之后,而且只有在身份已经确定的前提下才有意义:

  • 用户读取自己的数据。
  • 用户更新自己的资料。
  • 老师查看自己学生的记录。
  • 同样的请求换成学生发出,被拒绝。

一旦这一步出错,人们就会做出不该做的事。在 STRIDE 里这叫权限提升;在 OWASP Top 10 里这叫访问控制失效,而这一项已经连续多年排在榜首。

Juno决定你能做什么 注意,同一个人根据他请求的内容不同,得到的答案也不同。

登录状态本身不是一项单一的权限,它只是一个起点,每次请求都要重新问一遍那个独立的问题。

Juno决定你能做什么 常见的 bug 是只检查"这个人是否登录了",然后就到此为止。一个通过 id 加载记录、并返回给任何已登录用户的路由,就是"有身份验证、完全没有授权"的典型例子。

对每一个接收 id 的处理函数都该问一句:它有没有确认这个用户确实拥有这条记录?如果答案是没有,这个漏洞是有名字的,叫不安全的直接对象引用(insecure direct object reference),也是访问控制失效最常见的表现形式。

Juno决定你能做什么 授权判断放在代码的哪个位置,比它具体是怎么写的更重要。散落在各个处理函数里的检查会逐渐失控,而那个漏掉检查的路由在被人发现之前根本看不出问题。

真正靠得住的模式是把判断集中起来:要么由一个策略层统一接受路由的询问,要么由数据层保证根本不会返回调用者无权访问的行——两者都能让"到底哪些接口检查了归属权"这个问题,只需看一个地方就能回答。

隐藏的按钮不是授权。它只是给守规矩的用户提供的一点方便,跟浏览器端校验一模一样,它背后的那个接口,只要有人直接调用它,依然会照样响应。

它们运行的先后顺序

每一个到达受保护资源的请求,都会依次经过这两道关卡:

text
request  ──▶  authentication  ──▶  authorization  ──▶  resource
              (who are you?)      (may you do this?)

两个例子能把这个流程说清楚。用户读取自己的资料:请求到达,身份验证确认了他是谁,授权确认这份资料确实属于他,访问被允许。

管理员访问一个仅限管理员的接口:请求到达,身份验证确认了他是谁,授权检查他是否有管理员权限,访问被允许。

这两个例子的四个步骤里,只有一步不同。身份检查完全一样,权限检查才是两个请求分道扬镳的地方。

身份验证很强但授权很弱是不安全的。授权很严格但身份验证不可靠则毫无意义。

Juno它们运行的先后顺序 这个顺序不是一种约定俗成,而是一种依赖关系。你不知道对方是谁,就没法判断他能做什么。

这就是为什么身份验证每次都排在最前面,也是为什么这一步出错会让后面所有环节都失去意义。

Juno它们运行的先后顺序 在 Express 应用里,这两步都是中间件(middleware),而它们在链条中的先后顺序,就是图里画的那个顺序。如果把授权检查放在身份验证之前,它检查的就是一个还没被识别出身份的用户。

中间件的顺序会直接影响行为,而不只是代码整洁与否——这一点在某人重新排列了路由定义、结果这个路由突然不工作了的时候,值得多想一想。

Juno它们运行的先后顺序 要留意那种基于过时身份信息做出的授权判断。会话或 token 是在登录时建立的这份声明,而附着在其上的角色信息,等到请求真正到达时,可能已经过时了几分钟甚至几个小时。

这份"时间差"是否可以接受,正是本节所有身份模型底下都绕不开的权衡。服务器端保存的会话可以被即时更新或销毁;自包含的 token 却做不到这一点——这也是为什么一个被解雇的员工手上的 token,会一直有效直到过期为止。

这是一个设计决策,而不只是实现细节,也正是接下来几章要讨论"身份信息该存在哪里"的原因。

动手试一试

下面是四个场景。对每一个场景,判断这个失败属于身份验证问题还是授权问题,并说出它对应 STRIDE 里的哪个字母。

  1. 一个登录表单对用户名 admin 接受任意密码。
  2. 一个已登录的顾客修改了 URL 里的 id,看到了另一个顾客的订单。
  3. 一个 API 接口接受完全不带 token 的请求。
  4. 一个客服人员可以删除账户,而这原本应该只有管理员才能做。
对照一下你的答案
#失败类型原因STRIDE
1身份验证应用无法判断到底是谁在敲键盘,所以任何人都能冒充成 admin仿冒(Spoofing)
2授权这个顾客的身份确实没错;问题出在没人检查过这份订单是不是属于他权限提升
3身份验证请求根本没有出示任何声明,接口完全不知道调用者是谁。正确的响应是 401仿冒(Spoofing)
4授权这个客服人员的身份被正确识别了,出错的是权限检查。正确的拒绝方式是 403权限提升

第 2 个场景有自己的专门名字:不安全的直接对象引用。用 OWASP 的说法,2 和 4 都属于访问控制失效。

第 2 和第 4 其实是同一类 bug 换了个样子出现,这也是访问控制失效之所以一直高居 OWASP 榜首的部分原因。在这两个案例里,应用其实都清楚地知道是谁在发起请求。

接下来会讲什么

身份验证只在登录时发生一次。授权则在之后的每一次请求中都会发生,而且每次都需要知道当前用户是谁。

这就留下了一个尴尬的空缺:HTTP 本身不会记住任何一次请求和下一次请求之间的关联。

应用是如何记住你的 讲的正是这个空缺带来的问题,也是本节接下来会讲到的三类解决方案。