「用微信登录」「用 GitHub 登录」——现代互联网的每一个产品都支持第三方登录,背后的标准就是 OAuth 2.0。它的核心思想是:用户不需要把密码交给第三方应用,而是通过授权服务器颁发一个受限的 Token,第三方应用用这个 Token 访问受保护的资源。本篇讲清 OAuth 2.0 的四种授权模式、Token 机制与安全实践。
四种授权模式:按客户端类型选择
授权码模式(Authorization Code)——最安全最完整的模式,适用于有后端的 Web 应用。流程:① 用户点击「用 GitHub 登录」→ 重定向到 GitHub 的授权页面;② 用户在 GitHub 上确认授权;③ GitHub 重定向回你的应用,携带授权码(code);④ 你的后端拿授权码 + client_secret 向 GitHub 换取 AccessToken(后端与 GitHub 直接通信,secret 不暴露给浏览器)。安全核心:授权码只能用一次,AccessToken 的交换发生在后端(client_secret 不经过浏览器)。
简化模式(Implicit)——适用于无后端的纯前端应用(SPA),省略了授权码换 Token 的步骤,直接在重定向的 URL fragment 中返回 AccessToken。安全问题:AccessToken 暴露在 URL 中(浏览器历史记录、Referer 头都可能泄露)——已被 OAuth 2.1 废弃,SPA 应使用 PKCE 扩展的授权码模式。
密码模式(Resource Owner Password Credentials)——用户直接把用户名密码交给客户端,客户端用密码换 Token——只适用于高度信任的官方客户端(同一公司的第一方应用),第三方应用绝对禁止。已被 OAuth 2.1 废弃。
客户端凭证模式(Client Credentials)——没有用户参与的机器间通信(服务到服务的 API 调用),客户端用自己的 client_id + client_secret 直接获取 Token——这是微服务间调用最常用的认证方式(本库语境的「服务间认证」)。
PKCE:授权码模式的移动端扩展
PKCE(Proof Key for Code Exchange,读作 pixy)解决了移动端/SPA 的一个安全漏洞:移动端应用无法安全存储 client_secret(嵌入在 APK/IPA 中可被逆向提取),而授权码在重定向过程中可能被恶意应用拦截。PKCE 的流程:① 客户端生成一个随机字符串 code_verifier,计算其 SHA-256 哈希 code_challenge;② 授权请求携带 code_challenge(不发 verifier);③ 授权码返回后,客户端用 code_verifier 换 Token;④ 授权服务器验证 SHA256(code_verifier) == code_challenge。安全核心:即使授权码被拦截,没有 code_verifier 就无法换 Token——相当于「一次性密码」的挑战-响应机制。OAuth 2.1 将 PKCE 列为所有公共客户端的必选项。
Token 安全:AccessToken 与 RefreshToken 的配合
AccessToken(访问令牌)——短生命周期(15 分钟到 2 小时),用于访问受保护资源(通常以 JWT 格式,本库语境的「自包含令牌」——不需要查库即可验证,但无法主动吊销)。RefreshToken(刷新令牌)——长生命周期(7 天到 30 天),用于 AccessToken 过期后获取新的 AccessToken——RefreshToken 的安全性比 AccessToken 更关键(它等于「长期密码」,泄露后果严重)。配合策略:AccessToken 短命(缩小泄露窗口)+ RefreshToken 长命但使用时轮换(每次用 RefreshToken 换新 AccessToken 时,旧 RefreshToken 作废、发新 RefreshToken——Refresh Token Rotation,检测重放攻击:如果旧的 RefreshToken 被再次使用,说明已泄露,立即吊销整个令牌族)。存储位置:Web 端 RefreshToken 存 HttpOnly Cookie(防 XSS 窃取),AccessToken 存内存(不持久化,页面刷新时用 RefreshToken 重新获取——不要存 localStorage,XSS 可以读取)。
OAuth 2.0 与 OIDC 的关系:认证与授权
OAuth 2.0 解决的是授权问题(允许第三方应用访问你的资源),但不解决认证问题(确认用户是谁)——用 GitHub 登录的登录部分需要 OIDC 补充。OIDC 是 OAuth 2.0 之上的身份层:在授权流程中增加一个 ID Token——一个 JWT 格式的令牌,包含用户的身份信息。OIDC 与 OAuth 2.0 的区别用一句话总结:OAuth 2.0 回答这个应用可以访问我的什么资源,OIDC 回答这个用户是谁。实际使用中的关系:用 GitHub 登录的流程实际上是 OAuth 2.0 加 OIDC——先用授权码模式获取 AccessToken 与 ID Token,然后用 ID Token 中的用户信息创建或关联本地账户——授权与认证在一次流程中同时完成。
OAuth 2.0 的 Scope:最小权限的实践
OAuth 2.0 的 Scope 是 AccessToken 的权限范围——「这个 Token 可以做什么」的声明。Scope 的设计原则:最小权限——只授予应用完成功能所需的最小权限集合(一个只读日历的应用不应请求写入日历的权限)。Scope 的实现:授权请求中携带 scope 参数(scope=read:calendar write:calendar),授权服务器根据用户确认的范围颁发 Token,资源服务器验证 Token 的 scope 是否包含所需的操作。Scope 的粒度设计:过粗(只有 read 和 write——权限太大)vs 过细(read:calendar、read:contacts、read:profile...——权限太碎难以管理)——实践中的平衡点是按资源类型分组(read:profile + read:posts + write:posts——按资源分组的读写权限)。Token 的 scope 验证:资源服务器在收到请求时,检查 Token 中的 scope 是否包含所需的操作——scope 不足则返回 403 Forbidden(不是 401 Unauthorized——scope 不足是「权限不够」不是「未登录」)。401 vs 403 的区分是 API 设计的基础:401 = 未认证(没有 Token 或 Token 过期),403 = 已认证但权限不足(scope 不够或角色不对)。
Token 安全:JWT 与 Opaque Token 的对比
OAuth 2.0 的 Token 有两种格式。JWT 自包含令牌的优势:自包含不需要查库验证、无状态资源服务器独立验证。JWT 的劣势:无法主动吊销(泄露后只能等过期或维护黑名单)、Token 体积大。Opaque Token 的优势:可以随时吊销(Token 本身不含信息验证需要查授权服务器的 Token Introspection 端点——吊销立即生效)、Token 体积小。Opaque Token 的劣势:每次验证需要网络调用。选型建议:内部微服务间通信用 JWT 性能优先;对外 API 用 Opaque Token 安全优先。混合方案:AccessToken 用 JWT 短命加 RefreshToken 用 Opaque 长命可吊销——兼得两者的优点。
OAuth 2.0 的 Scope 与 PKCE 的安全实践
OAuth 2.0 的 Scope 是 AccessToken 的权限范围。Scope 的设计原则是最小权限。PKCE 解决了移动端的安全漏洞:客户端生成随机字符串 code_verifier,计算 SHA-256 哈希 code_challenge;授权请求携带 code_challenge 不发 verifier;授权码返回后客户端用 code_verifier 换 Token;授权服务器验证 SHA256 匹配。安全核心:即使授权码被拦截没有 code_verifier 就无法换 Token。OAuth 2.1 将 PKCE 列为所有公共客户端的必选项。Token 存储:RefreshToken 存 HttpOnly Cookie 防止 XSS 窃取,AccessToken 存内存不持久化。
OAuth 2.0 的安全漏洞与防御
OAuth 2.0 有几种常见的攻击方式与防御。CSRF 攻击:攻击者伪造回调 URL 中的授权码——防御是在授权码交换时验证 state 参数(客户端生成的随机值,授权请求携带、回调时验证一致)。授权码拦截:恶意应用拦截重定向中的授权码——防御是 PKCE 扩展(本篇正文的挑战-响应机制)。Token 泄露:Token 在 URL 中传输可能被日志或代理记录——防御是 Token 通过 POST 请求体传递而非 URL 参数。开放重定向:授权服务器的 redirect_uri 验证不严格导致授权码被发送到攻击者的服务器——防御是严格匹配注册的 redirect_uri。
OAuth 2.0 的 Token 存储最佳实践
OAuth 2.0 的 Token 存储位置直接影响安全性。Web 应用的最佳实践:AccessToken 存内存(不持久化——页面刷新时用 RefreshToken 重新获取),RefreshToken 存 HttpOnly Secure SameSite=Strict 的 Cookie(JavaScript 无法读取——XSS 无法窃取)。SPA 应用的 PKCE 授权码流程与 Token 存储:AccessToken 存内存(sessionStorage 可选但不如内存安全),RefreshToken 存 HttpOnly Cookie。移动应用:AccessToken 与 RefreshToken 存安全的本地存储(iOS 的 Keychain、Android 的 EncryptedSharedPreferences)。Token 过期后的静默刷新:AccessToken 过期时客户端用 RefreshToken 静默获取新的 AccessToken(用户无感知)——静默刷新失败时才重定向到登录页面。
OAuth 2.0 的 Token 过期与静默刷新
OAuth 2.0 的 AccessToken 过期后需要用 RefreshToken 静默刷新——静默刷新的实现细节值得了解。静默刷新的触发时机:AccessToken 过期前主动刷新(如过期前 5 分钟)或收到 401 响应后被动刷新。静默刷新的流程:用 RefreshToken 向授权服务器的 Token 端点发送 POST 请求(grant_type=refresh_token),获取新的 AccessToken 与新的 RefreshToken(Refresh Token Rotation 模式下旧 RefreshToken 作废)。静默刷新失败的处理:RefreshToken 也过期了需要重定向到登录页面重新认证。静默刷新的最佳实践:使用互斥锁防止多个请求同时触发刷新(并发刷新会导致 RefreshToken 被使用多次而失败),以及将刷新的响应缓存(同一时刻多个请求共享同一个新的 AccessToken)。
微服务间认证的选择:mTLS vs OAuth Client Credentials
微服务间的认证有两条主流路线。mTLS:双向 TLS 证书认证——服务间通信自动加密加身份验证(服务网格的原生能力——Istio 的 mTLS 自动管理证书的生命周期)。OAuth Client Credentials:服务用 client_id 加 client_secret 获取 AccessToken然后用 Token 调用其他服务——更灵活(可以控制 scope)但需要管理 Token 的获取与刷新。两者的对比:mTLS 的安全更强(双向证书验证不易伪造),OAuth Client Credentials 的粒度更细(scope 可以精确到 API 级别)。实际中两者可以结合:mTLS 保护传输层,OAuth 保护应用层——零信任架构的推荐实践是 mTLS 为基础 OAuth 为授权。
小结
OAuth 2.0 收成四条:四种授权模式按客户端类型选择——授权码模式(有后端 Web 应用)、PKCE 授权码(移动端/SPA,OAuth 2.1 强制)、客户端凭证(机器间调用)、密码模式与简化模式已被废弃;PKCE 用挑战-响应机制防授权码拦截——移动端与 SPA 的必选项;AccessToken 短命 + RefreshToken 长命且轮换——缩小泄露窗口 + 检测重放攻击;Token 存储——RefreshToken 存 HttpOnly Cookie、AccessToken 存内存,不存 localStorage。OAuth 2.0 的设计哲学:授权与认证分离(用户认证由授权服务器负责,资源访问由 AccessToken 授权),最小权限(Token 只授权必要的 scope),安全层层加码(secret → PKCE → rotation)。
参考资料
- RFC 6749: The OAuth 2.0 Authorization Framework —— OAuth 2.0 的权威规范
- RFC 7636: PKCE —— PKCE 扩展的规范
- OAuth 2.1 Draft —— 废弃了 Implicit 与 Password 模式
- 本库前篇:《HTTPS》《Tcp/Ip 协议精解》
读者留言
COMMENTS 暂无还没有留言,来说第一句?