OAuth 授权不应该只有“全部同意”或“完全拒绝”两个按钮。Cloudflare OAuth 现在支持可选 scopes,应用可以围绕当前任务请求更精确的权限,让用户更清楚地知道应用要访问什么,也让开发者更容易构建安全的授权流程。
授权范围应该服务于当前任务
传统的 OAuth 流程经常在首次登录时一次性申请一大组权限:读取账户、修改配置、管理 DNS,甚至访问与当前功能无关的资源。这样做虽然实现简单,但会带来两个问题:用户很难判断权限是否合理,应用一旦发生漏洞,攻击面也更大。
可选 scopes 的核心思路是把权限和任务绑定起来。例如:
- 查看站点状态,只申请读取相关权限。
- 修改 DNS 记录时,再申请 DNS 写入权限。
- 执行高风险操作时,单独触发一次更明确的授权。
这符合最小权限原则,也让授权页上的信息更容易被用户理解。一个好的 scope 名称应该能回答“应用准备做什么”,而不只是暴露内部资源模型。
设计渐进式 consent flow
可以把 OAuth 授权拆成三个阶段:
- 应用启动时只请求完成基础功能所需的 scopes。
- 用户进入特定任务后,检查当前 token 是否包含对应权限。
- 如果权限不足,引导用户重新进入 OAuth 授权流程,只增加当前任务需要的可选 scopes。
这里有一个重要边界:可选 scope 不等于可以绕过用户同意。每次新增权限都应该经过 OAuth 提供方的正式授权流程,并且应用不能把“暂时不授权”设计成无法使用基础功能的强制障碍。
应用还需要处理这些情况:
- 用户拒绝了可选权限,基础功能仍然可用。
- 用户授予了部分权限,应用按实际授权结果决定界面和操作。
- 用户撤销了授权,后续 API 请求返回未授权错误时需要重新引导。
- scope 配置发生变化,旧 token 不一定自动拥有新增权限。
一个可改造的授权 URL 示例
下面的 Python 示例只使用标准库,用来演示如何根据当前任务生成 OAuth 授权 URL。Cloudflare OAuth 的具体授权端点、回调地址和 scope 名称应以实际应用配置和官方文档为准,因此示例中的环境变量和 scope 是占位值。运行前,把它们替换成你的真实配置。
import os
from urllib.parse import urlencode
CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
AUTHORIZATION_ENDPOINT = os.environ["OAUTH_AUTHORIZATION_ENDPOINT"]
REDIRECT_URI = os.environ["OAUTH_REDIRECT_URI"]
TASK_SCOPES = {
"read_status": ["account:read"],
"edit_dns": ["account:read", "dns:write"],
}
def build_authorization_url(task: str, state: str) -> str:
try:
scopes = TASK_SCOPES[task]
except KeyError as exc:
raise ValueError(f"unsupported task: {task}") from exc
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": " ".join(scopes),
"state": state,
}
return f"{AUTHORIZATION_ENDPOINT}?{urlencode(params)}"
if __name__ == "__main__":
print(build_authorization_url("edit_dns", state="replace-with-server-side-random-state"))
可以这样运行:
export OAUTH_CLIENT_ID="your-client-id"
export OAUTH_AUTHORIZATION_ENDPOINT="https://provider.example.com/oauth/authorize"
export OAUTH_REDIRECT_URI="https://app.example.com/oauth/callback"
python oauth_url.py
示例中使用 state 防止跨站请求伪造。生产环境不能使用固定字符串,应在服务端生成不可预测的随机值,并把它和当前用户、目标任务、过期时间关联保存。回调时要验证 state,再用一次性的 authorization code 换取 token。
把 scope 检查放在业务边界
不要只在前端隐藏“修改 DNS”按钮,然后假设权限已经安全。真正的检查应该发生在服务端调用 Cloudflare API 之前。前端可以根据授权状态改善体验,但服务端必须把 token 的实际权限视为最终依据。
可以把任务权限建模成一个明确的映射:
REQUIRED_SCOPES = {
"view_status": {"account:read"},
"update_dns": {"account:read", "dns:write"},
}
def can_run_task(task: str, granted_scopes: set[str]) -> bool:
required = REQUIRED_SCOPES.get(task)
if required is None:
return False
return required.issubset(granted_scopes)
调用流程可以是:
用户点击“更新 DNS”
-> 服务端读取当前 token 的授权信息
-> 已包含 dns:write:执行操作
-> 缺少 dns:write:返回 needs_consent
-> 前端跳转到只申请 DNS 写入权限的 OAuth URL
实际项目还应记录授权时间、scope 集合和 token 状态,但不要把 access token 写入日志。对高风险操作,可以额外要求用户确认目标区域、记录内容或变更范围,避免“拥有权限”被误解为“任何操作都无需确认”。
采用前的检查清单
- 基础登录和核心读取能力是否只申请必要 scopes?
- 每个可选 scope 是否对应一个清晰、可解释的任务?
- 用户拒绝可选权限后,基础流程是否仍能正常工作?
- 服务端是否验证了
state、回调参数和 token 状态? - 缺少权限时,应用是否能返回明确的重新授权入口?
- scope 变更、token 撤销和部分授权是否有测试覆盖?
- 日志、缓存和错误信息中是否避免泄露 access token?
可选 scopes 的价值不只是把授权页拆得更细,而是把权限申请重新放回业务语境:用户做什么任务,应用就申请完成这个任务所需的权限。这样既能减少过度授权,也能让 consent flow 更容易解释、测试和维护。