从“一次全给”到按任务授权:Cloudflare OAuth 可选权限的实践指南

2026-08-21 39 预计阅读时间: 1 分钟
来源: blog.cloudflare.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

OAuth 授权不应该只有“全部同意”或“完全拒绝”两个按钮。Cloudflare OAuth 现在支持可选 scopes,应用可以围绕当前任务请求更精确的权限,让用户更清楚地知道应用要访问什么,也让开发者更容易构建安全的授权流程。

授权范围应该服务于当前任务

传统的 OAuth 流程经常在首次登录时一次性申请一大组权限:读取账户、修改配置、管理 DNS,甚至访问与当前功能无关的资源。这样做虽然实现简单,但会带来两个问题:用户很难判断权限是否合理,应用一旦发生漏洞,攻击面也更大。

可选 scopes 的核心思路是把权限和任务绑定起来。例如:

  • 查看站点状态,只申请读取相关权限。
  • 修改 DNS 记录时,再申请 DNS 写入权限。
  • 执行高风险操作时,单独触发一次更明确的授权。

这符合最小权限原则,也让授权页上的信息更容易被用户理解。一个好的 scope 名称应该能回答“应用准备做什么”,而不只是暴露内部资源模型。

可以把 OAuth 授权拆成三个阶段:

  1. 应用启动时只请求完成基础功能所需的 scopes。
  2. 用户进入特定任务后,检查当前 token 是否包含对应权限。
  3. 如果权限不足,引导用户重新进入 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 更容易解释、测试和维护。


相关推荐