OAuth 与委托身份认证
每一个"使用 Google 登录"按钮,本质上都是一个不打算自己做密码系统的决定。
你的应用不检查密码,不验证这个人是谁,也从不存储任何凭证。它只是去问一个自己已经信任的服务提供方,然后接受对方给出的答案。
这背后的协议是 OAuth,而这个流程涉及的环节,比一个按钮所暗示的要多得多。
两种不同的授权
几乎所有人都会在这里犯迷糊,值得先讲清楚,后面的流程才能说得通。
你一定见过这样的同意页面:一个应用请求获取你的姓名、头像和邮箱地址。有时候还不止这些,联系人、日历,甚至收件箱里的每一封邮件都可能被要求授权。
那个页面进行的是授权,和你应用里的角色体系没有任何关系。它决定的是你的应用可以访问用户 Google 账户里的哪些内容,这些权限被称为scopes(授权范围)。
你自己应用里的管理员和普通用户角色,仍然是在登录完成之后由你的后端来决定的,和前面几章讲的完全一样。
OAuth 负责授权,OIDC 负责身份认证
严格来说,OAuth 授予的是委托授权(authorization):允许你的应用访问某项资源的权限。构建在它之上的身份层叫做 OpenID Connect,也就是 OIDC,那些"使用……登录"按钮走的其实是 OIDC。
这个区分在代码层面很重要。OAuth 的访问令牌(access token)说的是你的应用可以访问什么;OIDC 的ID 令牌(ID token)说的是这个用户是谁。把前者当作身份证明来用,是一个常见但真实存在的错误。
Google 决定应用能看到你 Google 账户里的哪些信息;你的应用决定这个人登录进来之后能做什么。问题不同,决定的地方不同,答案也不同。
往返流程
一共八个步骤,用户会在中途离开你的应用:
- 用户在你的应用里点击按钮。
- 你的应用把用户重定向到 Google。
- 用户直接在 Google 上登录,你的应用完全看不到这个过程。
- Google 展示同意页面,列出你请求的授权范围。
- 如果用户同意,Google 会把一个一次性的批准码发给你的服务器。这个批准码不能让任何人登录,它只代表用户同意了授权,仅此而已。
- 你的服务器把这个批准码连同你应用自己的密钥凭证一起发回给 Google。
- 这些凭证向 Google 证明了这个请求确实来自你的后端。
- Google 回复一个 ID 令牌,说明这个用户是谁,你的应用据此让用户登录。
第五步到第八步这两次交接,是最值得搞懂的部分。批准码经过用户的浏览器传递,单独拿到手毫无用处;真正关键的令牌是在服务器和服务器之间交换的,浏览器从头到尾都看不到它。
这就是它的吸引力所在:你根本没收到过的凭证,是不可能泄露出去的。
三种常见的出错方式
把用户送走,再指望服务提供方把他们送回来,这个过程悄悄留下了三个漏洞。
| 出问题的地方 | 为什么重要 | 解决办法 | |
|---|---|---|---|
| 1 | 请求的权限太多 | 超出应用实际使用范围的 scope,意味着一旦泄露,暴露的信息会远超应有的程度,而且用户也会因为应用显得"多管闲事"而失去信任。信息泄露 | 只请求你的功能真正需要用到的最小权限集合 |
| 2 | 批准码泄露 | 批准码可能出现在地址栏、浏览器历史记录、分析工具或服务器日志里。任何人只要在它过期之前拿到它,就能以该用户的身份完成登录。身份伪装 | 不要让它出现在任何会被记录的地方,并且立刻拿去交换令牌 |
| 3 | 不加检查地信任 ID 令牌 | 任何人都能伪造一个看起来正确的令牌,但只有 Google 能给令牌签名。不加验证就接受,等于给了攻击者冒充任何人的机会。身份伪装 | 在信任令牌里的任何内容之前,先验证签名、受众(audience)和过期时间 |
第三个陷阱最致命,它会让整个模型失效。委托身份认证的意义,就在于由一个可信的第三方为用户背书;跳过验证这一步,等于变成了信任发送请求的任何人,而不是那个可信第三方。
任何人都能写一张便条。检查签名才是你确认这张便条确实来自 Google 的办法,跳过这一步,就等于相信一个你从没见过的笔迹。
练习一下
来自一次真实 OAuth 集成的五个场景。说出对应的陷阱、风险和解决办法。
- 你的应用只需要一个邮箱地址就能让用户登录,但 OAuth 请求里同时要求了联系人、日历和 Drive 文件的权限。
- 同意页面警告说,你的应用想要获得删除日历事件的权限。而你的应用从来没用过日历功能。
- 登录之后,页面短暂地在地址栏里显示了那个生命周期很短的批准码。
- 你的后端收到 ID 令牌后,没检查签名,也没检查签发对象是谁,就直接接受了。
- 你的后端接受了一个一小时前就已经过期的 ID 令牌。
对照答案
| # | 陷阱 | 风险 | 解决办法 |
|---|---|---|---|
| 1 | 请求的权限太多 | 一旦令牌泄露,暴露的用户账户信息会远超应用实际所需 | 只请求某个功能真正用到的 scope |
| 2 | 请求的权限太多 | 同样的信息暴露问题,还额外让同意页面在用户使用应用之前就显得不值得信任 | 去掉没有用到的 scope |
| 3 | 批准码泄露 | URL 会被记录、保存和共享。任何人只要在批准码过期之前拿到它,就能完成登录 | 不要让批准码出现在 URL 里,拿到手立刻去交换 |
| 4 | 信任了未经验证的 ID 令牌 | 任何人都能伪造一个"看起来像令牌"的东西。不做签名和受众检查,攻击者就能冒充任何用户 | 在信任令牌之前,先验证签名和受众 |
| 5 | 信任了未经验证的 ID 令牌 | 一个旧令牌可以被反复重放,用来一次次冒充该用户登录 | 检查过期时间,拒绝任何已过期的令牌 |
第 2 项和第 1 项其实是同一个陷阱,只是后果不同,这也是为什么这份清单更适合当作三个问题、而不是五个问题来读。第 4 项和第 5 项也是同一个陷阱:验证本来就是一组检查,漏掉其中任何一项,都等于没有做验证。
接下来往哪走
三种模型,从不同的方向解决同一个问题:服务器自己记住状态、客户端随身携带证明,或者由一个服务提供方来背书。
如何选择身份认证模型把这三种模型放在一起对比,讲清楚每一种分别适合在什么情况下使用。

