第三方 逻辑分析

目标函数:controller/oauth.go:131

这个函数处理的是 OAuth 账号绑定流程,不是 OAuth 登录注册流程。它只会在用户已经登录的情况下执行:主入口 controller/oauth.go:43 会先校
验 state,然后判断 session 里是否已有 username,如果有,就进入 handleOAuthBind。

触发条件

入口逻辑在 HandleOAuth 中:

  1. 根据路由参数 provider 获取 OAuth provider。

  2. 校验 OAuth state,用于防 CSRF。

  3. 如果当前 session 中存在 username,说明用户已登录。

  4. 已登录时不走登录/注册,而是调用:

    handleOAuthBind(c, provider)

    所以 handleOAuthBind 的语义是:

    当前登录用户把第三方 OAuth 账号绑定到自己的系统账号。

    执行流程

    1. 检查 Provider 是否启用

如果当前 OAuth provider 被关闭,直接返回错误。

这里虽然主流程后面也会检查 provider enabled,但绑定分支提前 return 了,所以绑定函数里必须自己再检查一次。

2. 用授权码换 Token

从回调 URL 中读取 code,调用 provider 的 ExchangeToken 换取访问令牌。

如果失败,进入统一 OAuth 错误处理:

3. 用 Token 获取 OAuth 用户信息

返回的核心字段是 oauthUser.ProviderUserID,用于标识第三方平台上的用户 ID。

例如 GitHub 当前逻辑中,ProviderUserID 是 GitHub numeric id,同时 Extra[“legacy_id”] 保存旧逻辑使用的 GitHub login。

4. 检查该 OAuth 账号是否已被绑定

位置:controller/oauth.go:153

如果第三方账号 ID 已经存在绑定关系,拒绝绑定。

不同 provider 的检查方式不同:

  • 内置 provider:通常查 users 表上的字段,比如 github_id、discord_id、oidc_id。

  • 自定义 OAuth provider:查 user_oauth_bindings 表。

    5. 检查 legacy_id,避免迁移期重复绑定

    这是为了兼容历史数据。

    典型场景是 GitHub:

  • 老版本可能用 GitHub login 作为绑定 ID。

  • 新版本改成 GitHub numeric id。

  • 为避免同一个 GitHub 账号在迁移期间被重复绑定,会同时检查新 ID 和旧 ID。

    6. 从 Session 获取当前登录用户

    这里依赖 session 中存在 id,并且类型必须是 int。

    如果查不到用户或数据库错误,返回 common.ApiError。

    潜在风险:这里直接做了 id.(int) 类型断言。如果 session 异常、缺失或类型不是 int,会 panic。不过正常路径下,登录时 controller/
    user.go:97 会写入 id。

    7. 根据 Provider 类型执行绑定

    位置:controller/oauth.go:176

    这里分两类处理。

    自定义 OAuth Provider

    自定义 provider 不写 users 表字段,而是写 user_oauth_bindings 表。

    底层逻辑在 model/user_oauth_binding.go:108:

  1. 检查 provider_id + provider_user_id 是否已经绑定给其他用户。

  2. 如果当前用户没有该 provider 的绑定,则创建。

  3. 如果当前用户已有该 provider 的绑定,则更新 provider_user_id。

    也就是说,自定义 OAuth 支持“重新绑定同一个 provider 下的另一个第三方账号”。

    内置 OAuth Provider

    8. 返回绑定成功

整体流程图

  OAuth callback
      |
      v
  HandleOAuth
      |
      v
  校验 provider 是否存在
      |
      v
  校验 state
      |
      v
  session.username 存在?
      |
      +-- 否 --> OAuth 登录/注册流程
      |
      +-- 是 --> handleOAuthBind
                    |
                    v
                provider 是否启用?
                    |
                    v
                code 换 token
                    |
                    v
                token 拉取 OAuth 用户信息
                    |
                    v
                ProviderUserID 是否已被绑定?
                    |
                    v
                legacy_id 是否已被绑定?
                    |
                    v
                读取当前登录用户
                    |
                    v
                自定义 provider?
                    |
            +-------+--------+
            |                |
            v                v
    写 user_oauth_bindings   更新 users 表 provider 字段
            |                |
            +-------+--------+
                    |
                    v
                返回绑定成功

关键业务规则

  • 只有已登录用户才会进入绑定流程。

  • 一个 OAuth 第三方账号不能绑定到多个系统用户。

  • 自定义 OAuth provider 的绑定关系存储在 user_oauth_bindings。

  • 内置 OAuth provider 的绑定关系存储在 users 表对应字段。

  • GitHub 等 provider 支持 legacy_id 检查,用于防止历史 ID 迁移期间重复绑定。

  • 绑定成功后不会重新登录,也不会刷新 session 用户信息,只返回绑定成功。

    需要注意的点

  1. session.Get(“id”) 直接类型断言为 int,session 异常时可能 panic。
  2. 自定义 OAuth 的 UpdateUserOAuthBinding 支持覆盖当前用户已有绑定,等价于“重新绑定”。
  3. 内置 provider 的 user.Update(false) 会更新整个用户模型,具体更新范围取决于 model.User.Update 的实现。
  4. provider.IsUserIDTaken 和后续写入不是一个事务,理论上存在并发重复绑定竞争。不过数据库唯一索引或字段唯一约束是否兜底,需要看具体
    provider 对应字段和表结构。