OAuth2 Device Authorization Flow 深度解析
RFC 8628 · 适用于无浏览器或输入受限设备的授权流程
一、为什么需要 Device Flow
标准的 Authorization Code Flow 依赖浏览器重定向,用户需要在授权页面输入账号密码并点击同意。但对于以下场景,这套流程完全行不通:
- 智能电视 / 流媒体盒子:有屏幕,但没有键盘,无法输入复杂的 URL 和密码
- CLI 工具(如
gh auth login、AWS CLI SSO):运行在终端,无法弹出浏览器 - IoT 设备:资源受限,可能没有显示屏,更没有浏览器
- 游戏主机:手柄操作输入极不方便
Device Flow 的核心思路是把**“需要授权的设备"和"完成授权的浏览器”**彻底解耦——设备只负责展示一个短码并轮询,用户用另一台有浏览器的手机或 PC 完成授权。
💡 类比:你在电影院的自助取票机(受限设备)上扫码,用手机(有浏览器的设备)完成支付和身份核验,取票机等待确认后吐出票。取票机全程不需要输入密码,也不需要有屏幕键盘。
二、完整授权流程
角色说明
| 角色 | 说明 |
|---|---|
| 受限设备 | TV、CLI、IoT 等,发起授权请求并轮询 |
| 授权服务器 | 颁发 device_code / user_code,验证用户授权 |
| 用户 | 使用有浏览器的手机或 PC 完成授权 |
时序图
受限设备 授权服务器 用户(手机/PC)
│ │ │
│── ① POST /device_authorization ──────────────────────────>│
│ client_id, scope │ │
│ │ │
│<── ② device_code, user_code, verification_uri, expires_in, interval ──│
│ │ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ③ 设备展示给用户: │ │
│ │ 请访问:https://example.com/device │ │
│ │ 输入代码:BDFH-JLNP │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │<── ④ 用户打开 verification_uri ──│
│ │<── ⑤ 输入 user_code 并登录授权 ──│
│ │ │
│── ⑥ 轮询 POST /token ──────>│ │
│ device_code │ (用户尚未完成) │
│<── authorization_pending ──│ │
│ │ │
│ ... 按 interval 继续轮询 ...│ │
│ │ │
│── ⑧ 再次轮询 POST /token ──>│ │
│<── ⑨ access_token ─────────│ │
│ refresh_token │ │
│ │ │
│ ✓ 授权完成 │ │
分步说明
第 ① 步:设备请求授权
设备向授权服务器的 /device_authorization 端点发起 POST 请求:
POST /device_authorization HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=your_client_id&scope=read:profile
第 ② 步:获取 device_code 和 user_code
授权服务器返回:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "BDFH-JLNP",
"verification_uri": "https://example.com/device",
"verification_uri_complete": "https://example.com/device?user_code=BDFH-JLNP",
"expires_in": 900,
"interval": 5
}
第 ③ 步:设备展示短码
设备将 user_code 和 verification_uri 展示给用户,可以是屏幕文字、二维码或终端输出。
第 ④⑤ 步:用户在另一台设备上完成授权
用户用手机或 PC 打开 verification_uri,输入 user_code,完成登录和授权确认。
第 ⑥⑦ 步:设备轮询(等待中)
设备按 interval 秒间隔持续请求 /token:
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=your_client_id
在用户完成授权前,服务器返回 authorization_pending,设备继续等待。
第 ⑧⑨ 步:轮询成功,获得 Token
用户授权完成后,下次轮询即可获得 Token:
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read:profile"
}
三、关键参数说明
| 参数 | 归属方 | 说明 |
|---|---|---|
device_code | 设备持有 | 轮询时使用,用户不可见,高熵随机字符串 |
user_code | 展示给用户 | 短码,设计为易于手动输入(通常 8 位,含连字符) |
verification_uri | 展示给用户 | 用户在浏览器中打开的链接 |
verification_uri_complete | 可选,展示给用户 | 附带 user_code 的完整链接,可生成二维码,减少输入错误 |
interval | 设备遵守 | 轮询间隔秒数,必须严格遵守,不得低于此值 |
expires_in | 设备遵守 | device_code 和 user_code 的有效期(秒) |
四、轮询错误码处理
| 错误码 | 含义 | 处理方式 |
|---|---|---|
authorization_pending | 用户尚未完成授权 | 继续按 interval 轮询,不做任何改变 |
slow_down | 轮询过于频繁 | interval += 5 秒后继续轮询 |
expired_token | device_code 已过期 | 终止当前流程,重新发起(重新请求 device_code) |
access_denied | 用户明确拒绝授权 | 终止,向用户提示授权被拒绝 |
⚠️ 注意:
slow_down不是错误,是服务端在保护自己;收到时必须增加间隔,否则可能被封禁。
五、Python 完整示例
import time
import requests
CLIENT_ID = "your_client_id"
DEVICE_AUTH = "https://example.com/oauth/device_authorization"
TOKEN_URL = "https://example.com/oauth/token"
# ① 请求 device_code 和 user_code
resp = requests.post(DEVICE_AUTH, data={
"client_id": CLIENT_ID,
"scope": "read:profile",
})
data = resp.json()
device_code = data["device_code"]
user_code = data["user_code"]
verification_uri = data["verification_uri"]
interval = data.get("interval", 5) # 默认 5 秒
expires_in = data["expires_in"]
# ② 展示给用户(电视屏幕 / 终端输出)
print(f"请在浏览器打开:{verification_uri}")
print(f"并输入代码: {user_code}")
print(f"(代码 {expires_in} 秒内有效)")
# ③ 轮询直到成功或超时
deadline = time.time() + expires_in
while time.time() < deadline:
time.sleep(interval)
token_resp = requests.post(TOKEN_URL, data={
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": CLIENT_ID,
})
token_data = token_resp.json()
if "access_token" in token_data:
print("✓ 授权成功!")
print(f" access_token: {token_data['access_token']}")
print(f" refresh_token: {token_data.get('refresh_token', '—')}")
break
error = token_data.get("error")
if error == "authorization_pending":
print(" 等待用户授权...") # 继续轮询
elif error == "slow_down":
interval += 5 # 降低频率
print(f" 降速,新间隔 {interval}s")
elif error == "expired_token":
print("✗ 代码已过期,请重新发起授权流程")
break
elif error == "access_denied":
print("✗ 用户拒绝授权")
break
else:
print(f"✗ 未知错误:{token_data}")
break
else:
print("✗ 超时,用户未在有效期内完成授权")
六、安全注意事项
客户端(设备侧)
- user_code 有效期建议 ≤ 15 分钟,过期后需重新发起整个流程
- 严格遵守
interval,收到slow_down须立即加大间隔,不得继续原频率 - 优先使用
verification_uri_complete(可生成二维码),减少用户手动输入 - Token 获取后写入安全存储(Keychain / Keystore / OS 凭证管理器),不可明文存入文件
服务端(授权服务器侧)
- 对每个
device_code独立做速率限制,防止暴力轮询探测 user_code应设计为大小写不敏感,去除易混淆字符(0vsO,1vsIvsl)- 建议在授权页面展示设备信息(IP、请求时间、scope),帮助用户辨别合法请求
七、与其他流程的对比
| 对比维度 | Authorization Code + PKCE | Client Credentials | Device Flow |
|---|---|---|---|
| 是否需要浏览器 | ✅ 需要 | ❌ 不需要 | ❌ 不需要(另一台设备完成) |
| 是否有用户参与 | ✅ 有 | ❌ 无 | ✅ 有(在另一台设备上) |
| 适用端 | Web / 移动 App | 后端服务 | TV / CLI / IoT |
| 获得 Refresh Token | ✅ 是 | ❌ 通常无 | ✅ 是 |
| 规范出处 | RFC 6749 | RFC 6749 | RFC 8628 |
八、真实案例
| 产品 / 工具 | Device Flow 的具体体现 |
|---|---|
| GitHub CLI | gh auth login → 终端输出 code → 浏览器打开 github.com/login/device |
| Google TV | 电视屏幕显示 8 位码 → 手机打开 google.com/device 输入 |
| VS Code | Remote 插件授权 → 浏览器完成 GitHub / Microsoft 登录 |
| AWS CLI | aws sso login → 浏览器完成 SSO 授权 → CLI 自动获取临时凭证 |
| Spotify | 游戏主机、智能音箱登录 Spotify 账号 |
小结
Device Flow 的精妙之处在于对"授权者"和"被授权设备"的解耦:
device_code:设备和服务器之间的凭证,用户不可见user_code:人类可读的短码,连接"哪台设备在请求授权"和"用户的授权行为"- 轮询机制:设备无需监听回调,主动拉取结果,天然适合无公网地址的设备
2025 年选型建议:TV / 流媒体 / 游戏主机 / IoT 硬件,以及任何"有输出但输入受限"的客户端,Device Flow 是唯一合适的选择。CLI 工具也应优先考虑 Device Flow 而非密码模式。

1818

被折叠的 条评论
为什么被折叠?



