题干与适用场景
这是 OAuth 协议实现与安全设计题。RFC 8628 面向智能电视、媒体主机、打印机等无法方便输入或运行浏览器的设备:设备向授权服务器申请 devicecode 与 usercode,用户在另一台设备的浏览器中完成登录和授权,原设备再轮询令牌端点。设备通常是 public client,不能把长期客户端密钥藏在固件里。题目不要求用 Device Flow 替代能运行系统浏览器的原生应用授权码流程。
面试官考察点
- 能否正确区分设备码、用户码、验证 URI、访问令牌和刷新令牌的生命周期。
- 能否把
authorizationpending、slowdown、expiredtoken、accessdenied映射到状态机。 - 能否设计有界轮询、退避、幂等和并发控制,避免令牌端点被打爆。
- 能否识别用户码钓鱼、设备冒认、暴力猜码和 public client 的风险。
回答前需要澄清的问题
先确认设备是否能显示 URI 和用户码、是否能发起 HTTPS、是否有可靠时钟,以及授权服务器是否支持 OIDC 登录。再确认设备需要访问哪些资源、令牌是否必须绑定设备、用户码有效期和轮询间隔由谁配置、是否允许取消。若设备已有系统浏览器,应优先考虑 RFC 8252 的授权码流程,而不是强行使用 Device Flow。
30 秒回答框架
我会实现两个端点:设备授权端点返回短生命周期 devicecode、可人工输入的 usercode、verificationuri、过期时间和最小轮询间隔;令牌端点只接受 device code 并返回令牌或标准错误。服务器保存哈希后的设备码、状态、客户端和过期时间。设备按服务器给出的 interval 轮询,收到 authorizationpending 继续,slow_down 增加间隔,成功后停止;过期、拒绝或无效立即终止。用户页面显示正在授权的设备信息,降低钓鱼风险。
分步骤深入解答
1. 建模参与者和一次授权会话
参与者包括受限设备、授权服务器、用户浏览器和资源服务器。设备授权端点创建一次会话,生成高熵随机 devicecode 与便于输入的 usercode,关联客户端、请求范围、创建时间、过期时间和轮询状态。数据库只保存 device code 的不可逆摘要,避免读库泄露即可直接换取令牌;用户码需要独立索引和严格尝试次数。
2. 设计设备授权响应
响应至少包含 devicecode、usercode、verificationuri、expiresin 和 interval。verificationuricomplete 可以提供带用户码的便捷链接,但更应在设备屏幕和用户页面同时显示相同的短码,要求用户确认正在操作的设备。设备应安全显示码,避免日志、分析事件或截图泄露;服务器不应把访问令牌放进此响应。
3. 处理用户交互与授权绑定
用户打开验证 URI 后登录并看到客户端名称、请求范围和设备信息。授权页面必须让用户确认该设备在自己身边,防止攻击者诱导用户为另一台设备授权。批准后只把会话状态改为 AUTHORIZED,不直接把令牌通过浏览器回传给设备。拒绝操作写成终态 DENIED,并让设备在下一次轮询得到明确错误。
4. 定义令牌端点状态机
令牌端点用标准错误驱动设备状态:
poll(device_code):
session = lookup(hash(device_code))
if session is missing: return invalid_grant
if now >= session.expiresAt: return expired_token
if session.status == DENIED: return access_denied
if session.status != AUTHORIZED: return authorization_pending
atomically mark CONSUMED
return issueAccessAndRefreshTokens(session)同一 device code 的成功兑换必须原子化,避免两个设备线程拿到两组令牌。若设备码只允许一次兑换,重复请求返回 invalid_grant 或协议约定的已消费错误;不要在失败重试中重新创建授权会话。
5. 实施轮询退避和服务器保护
设备首次轮询不得早于响应中的 interval。收到 authorizationpending 时保持相同间隔;收到 slowdown 时至少增加 5 秒,再继续轮询。设备端还要设置总截止时间、网络超时和抖动,服务器端按 device code、客户端和 IP 限速。令牌端点应返回缓存禁止策略,避免代理重放状态响应;服务端高峰时宁可返回可重试错误,也不能为每次轮询创建昂贵后台任务。
6. 处理过期、取消和恢复
设备必须把 expiredtoken、accessdenied 和永久 invalid_grant 视为终态,清理本地 device code,不再继续轮询。用户可以在设备上选择取消,服务器将会话标记为 CANCELLED,后续请求不应恢复。网络断开时保留截止时间并从同一 device code 恢复,不能自动重新申请多个会话导致用户授权错设备。设备重启后若本地没有安全保存会话状态,应重新开始并通知用户。
7. 处理 public client 与反钓鱼安全
设备端通常无法保护客户端密钥,应按 public client 对待;访问范围最小化,刷新令牌采用轮换和撤销策略。用户码要足够短以便输入,又要有足够熵抵抗在线猜测;验证端点对错误次数和 IP 组合限速。页面显示设备名称、型号或一次性短码,并建议用户确认设备屏幕上的码,降低远程钓鱼。日志只记录会话 ID、结果和哈希后的标识,不能记录原始 device code 或令牌。
8. 可观测性和测试
指标包括设备授权创建率、授权完成率、平均完成时间、每个会话轮询次数、slow_down 比例、过期与拒绝比例,以及令牌兑换冲突。测试覆盖重复兑换、超时、时钟偏差、网络重试、恶意猜码、并发轮询、用户拒绝、服务器重启和服务端限流。端到端测试必须确认用户批准的设备与最终令牌会话一致,不能只验证两个 HTTP 端点分别返回 200。
高质量示范回答
我会按 RFC 8628 建立一次性会话:设备授权端点返回高熵 devicecode、短用户码、验证 URI、过期时间和最小轮询间隔,服务端保存哈希后的设备码和明确状态。用户在手机浏览器登录后看到客户端、范围和设备信息并批准;令牌端点只接受 device code,authorizationpending 继续轮询,slowdown 增加至少 5 秒间隔,expiredtoken、access_denied 和永久无效立即终止。成功兑换在事务中原子消费 device code,避免并发发放两组令牌。设备按 public client 处理,最小化范围、轮换刷新令牌、限制猜码并要求用户核对设备屏幕上的短码。指标和测试覆盖全流程一致性、退避、恢复和钓鱼边界。
常见错误
- 把 device code 当作用户可输入的短码,导致日志和 UI 暴露高价值凭证。
- 忽略
slow_down,固定高频轮询令牌端点。 - 把所有错误都当作继续重试,过期或拒绝后仍然轮询。
- 用设备端静态客户端密钥假装它是 confidential client。
- 用户批准后直接把令牌放入浏览器 URL 或页面脚本。
- 并发轮询未原子消费 device code,发放多组刷新令牌。
- 只测试轮询接口,不验证用户批准的设备和最终令牌是否绑定。
追问及应对
如何选择用户码长度和有效期?
在可输入性、在线猜测成本和用户完成时间之间权衡。使用字符集避免易混淆字符,服务器限制每个码的失败次数和总尝试速率;有效期要覆盖用户拿手机、登录和确认设备的时间,但不能无限延长钓鱼窗口。最终数值应由威胁模型和实际完成时间校准。
如何避免恶意客户端占满授权会话?
按客户端注册、IP、设备指纹和账号维度限制创建速率,限制每个主体的未完成会话数,并让过期会话异步清理。创建接口先验证客户端和 scope,再生成会话;不能把垃圾 device code 的清理压力转移给令牌端点。
用户批准后设备离线,之后如何恢复?
会话状态保留到过期时间,设备恢复网络后可继续使用原 device code 轮询。服务器应返回成功令牌或明确终态;设备端为令牌兑换请求设置幂等处理,避免网络响应丢失后重复消费。超过有效期则重新开始授权并显示新的短码。
verificationuricomplete 有什么风险?
它把用户码放入链接,减少输入但增加链接泄露和远程钓鱼风险。页面仍应显示短码并要求用户确认设备屏幕上的相同值;不要在分析、Referer、历史记录或第三方跳转中暴露完整链接。可按设备能力和威胁模型决定是否提供。
为什么不让手机把访问令牌直接传给设备?
跨设备传输令牌会扩大浏览器、二维码、剪贴板和中间页面的暴露面,也难以保证令牌交给的是正确设备。Device Flow 让设备用自己的 device code 在 TLS 连接中兑换令牌,用户页面只改变服务器状态;若产品需要近场绑定,应另加经过认证的安全通道。
如何监控轮询系统是否退避正确?
记录会话级轮询次数、时间间隔分布、slowdown 后的实际间隔、终态耗时和按客户端聚合的失败率。用受控测试注入重复轮询,验证服务端返回 slowdown 后客户端至少增加 5 秒且不会产生同步风暴;把异常高频客户端隔离,不用全局放宽间隔。