Skip to content

🔐 OAuth 集成

app/services/oauth/ 是通用 OAuth 2.0 抽象层,让 Bangumi、Trakt 等不同授权方共用同一套授权 URL 构建 / 令牌交换 / 刷新 / CSRF state 管理逻辑,仅需登记一个 OAuthProvider 即可接入新来源。

技术栈

  • 协议:OAuth 2.0 Authorization Code Flow
  • HTTP 客户端httpx(同步,30s 超时)
  • state 存储:SQLite oauth_states 表(带 TTL,原子消费)
  • 回调策略:动态 redirect_uri(前端传入,绑定到 state 落库)

OAuthProvider:提供方配置

app/services/oauth/provider.py 用 dataclass 定义单个 OAuth 提供方的静态配置,OAuthProviderRegistry 是提供方注册表。

字段类型说明
namestr提供方标识(如 "bangumi""trakt"
authorize_urlstr授权端点
token_urlstr令牌端点
redirect_pathstr回调路径
scopeslist[str]OAuth scopes
extra_auth_paramsdict额外授权参数
get_credentialsCallable[[], tuple[str, str]]返回 (client_id, client_secret)
get_redirect_uriCallable[[], str]返回默认回调地址

get_credentials / get_redirect_uri 以可调用形式提供,便于延迟导入,避免与具体媒体源模块形成循环依赖。内置提供方在 app/services/oauth/providers.py 登记:

python
BANGUMI_PROVIDER = OAuthProvider(
    name="bangumi",
    authorize_url="https://bgm.tv/oauth/authorize",
    token_url="https://bgm.tv/oauth/access_token",
    redirect_path="/api/oauth/bangumi/callback",
    scopes=[],
    extra_auth_params={"response_type": "code"},
    get_credentials=_bangumi_credentials,   # 延迟导入 app.services.bangumi.auth
    get_redirect_uri=_bangumi_redirect,
)

OAuthService:通用服务

app/services/oauth/service.py 是无状态的服务层,所有 state 落库到 oauth_states 表,通过 get_oauth_service() 获取共享实例。核心能力:create_state / consume_state(CSRF 防护)、build_authorize_urlexchange_coderefresh_token

常见参数

参数默认值说明
OAUTH_STATE_TTL600state 有效期(秒),防止重放
刷新提前量3600token 临近过期阈值(提前 1 小时刷新)
HTTP 超时30.0令牌交换/刷新的 HTTP 超时

state 原子消费

consume_state 通过 delete_oauth_staterowcount 判断是否消费成功,避免 SELECT-then-DELETE 在并发下双重消费 state 导致 CSRF 防护失效。


如何接入新 OAuth 提供方

以接入「MyAnimeList」为例,需要 3 个改动点:

1. 登记提供方

app/services/oauth/providers.py 追加一个 OAuthProvider,并在 __init__.py 注册:

python
MAL_PROVIDER = OAuthProvider(
    name="mal",
    authorize_url="https://myanimelist.net/v1/oauth2/authorize",
    token_url="https://myanimelist.net/v1/oauth2/token",
    redirect_path="/api/oauth/mal/callback",
    scopes=["read"],
    extra_auth_params={"response_type": "code"},
    get_credentials=_mal_credentials,   # 延迟导入,返回 (client_id, client_secret)
    get_redirect_uri=_mal_redirect,     # 延迟导入,返回默认回调地址
)

2. 实现特有 auth 服务

参考 app/services/bangumi/auth.pyBangumiAuthService,实现:

  • get_app_credentials() / get_redirect_uri():凭证解析(配置文件 → 环境变量优先级)
  • 令牌落地:将 token 写入对应数据库表(upsert 账号,保留用户配置)
  • 用户信息补全:若提供方令牌响应不含 username,需调用用户信息接口补全(参考 _fetch_me_info
  • Token 刷新:per-section 锁 + double-check 避免并发重复刷新

3. 实现 API 路由

参考 app/api/bangumi_oauth.py,实现 /start/callback/close/disconnect 四个接口。通用 OAuthService 已处理 state 管理、授权 URL 构建、令牌交换,路由层只需串联流程。

redirect_uri 白名单

/start 接口需校验 redirect_uri 白名单(必须以指定回调路径结尾),防止授权码被导向外部站点。


完成效果

新提供方即可复用通用 state 管理、授权 URL 构建、令牌交换与刷新流程,路由层只需实现特有逻辑(账号落地、用户信息补全),无需重复造轮子。