OAuth 授权流程示意图


对接实例:GitHub

准备工作

  1. 在 GitHub 注册 OAuth 应用,获取 client_id 和 client_secret。
  2. 在项目环境变量中配置凭据和回调地址;不要把真实密钥提交到代码仓库。

授权流程

  1. 前端向后端请求 GitHub 授权 URL。
  2. 后端生成包含 client_id、scope、state 和 redirect_uri 的授权 URL。
  3. 浏览器跳转到 GitHub 授权页面。
  4. 用户同意后,GitHub 重定向到回调地址并附带 code 和原始 state。
  5. 后端校验 state,再用 code 向 GitHub 兑换访问令牌。
  6. 后端使用访问令牌获取 GitHub 用户信息,并创建或关联本地用户。
  7. 后端为本系统生成自己的会话或访问令牌;GitHub 的访问令牌不应直接当成本系统的登录凭据。

生成授权 URL

def get_authorization_url(self, state: str) -> str:
    """生成 GitHub 授权 URL。"""
    params = {
        "client_id": self.client_id,
        "redirect_uri": self.redirect_uri,
        "scope": "user:email",
        "state": state,
    }
    return f"{self.AUTHORIZE_URL}?{urlencode(params)}"

用授权码兑换访问令牌

def exchange_code_for_token(self, code: str) -> str:
    """用授权码换取 access_token。"""
    response = requests.post(
        self.TOKEN_URL,
        data={
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "code": code,
            "redirect_uri": self.redirect_uri,
        },
        headers={"Accept": "application/json"},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()["access_token"]

获取用户信息

def get_github_user_info(self, access_token: str) -> dict:
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json",
    }
    response = requests.get(self.USER_API_URL, headers=headers, timeout=10)
    response.raise_for_status()
    user_data = response.json()

    # 若公开资料没有邮箱,再从邮箱接口中选择已验证的主邮箱。
    if not user_data.get("email"):
        emails_response = requests.get(
            self.USER_EMAILS_URL, headers=headers, timeout=10
        )
        emails_response.raise_for_status()
        for email_data in emails_response.json():
            if email_data.get("primary") and email_data.get("verified"):
                user_data["email"] = email_data["email"]
                break

    return user_data

实现时还应使用短期、一次性的 state 防止 CSRF,校验回调地址,并通过安全的环境变量或密钥管理服务保存 client_secret。