【Web全栈进阶】OAuth实战——GitHub第三方登录

"用GitHub账号登录"是每个网站都想要的体验:用户不用注册、密码永远不会经过你的服务器。

本篇走完授权码流程全链路 ,并把JWT接进来------OAuth负责 "他是谁",JWT负责 "他还在"。

🎯 本篇产出:GitHub登录入口 + 回调处理 + 自动建本地用户 + 签发自有JWT。含代码约110行。


📌 太长不看版(给想快速上手的你)

项目信息 一句话说明
本篇目标 给早报站加上GitHub第三方登录
代码行数 ~110行(含注释)
依赖 requests(第11篇老朋友)
核心功能 授权码流程 + find-or-create + 签发自有JWT
跑起来的命令 /docs → GET /auth/github → 浏览器走流程
核心知识点 OAuth授权码六步、client_secret只走服务器、find-or-create
做完你能得到 一套标准的第三方登录方案,换平台只改端点

🚨 核心认知 :OAuth只是 "认证",进了门之后发什么令牌、给什么权限,完全是你自己说了算。


一、先想清楚:OAuth和JWT的分工

JWT是 "自己发令牌 ":你的站有自己的用户体系,登录后你发令牌。

OAuth是 "别人帮你认证":GitHub确认了 "这人是谁",然后把这个结论告诉你。

🎯 两者不是二选一,而是组合

标准姿势:

📌 一次OAuth换一张长期有效的 "本地通行证",这是全世界的标准做法。


二、授权码流程:六步走(核心概念课)

主角有三个:用户(浏览器)、你的站(Client)、GitHub(授权服务器)。

📋 六步全流程

🚨 两个必须记住的安全点

# 安全点 说明
① 第④步必须在服务器做 换token需要client_secret------它和你的JWT_SECRET同级别,永远只活在服务器和.env里
② state参数防CSRF 攻击者可以伪造 "回调你的服务器"------带一个自己生成的state,回调时比对,对不上就拒绝(本篇先演示流程,state完整校验放练习①)

三、第0步:注册OAuth App

要让你的早报站支持"用GitHub登录",首先得在GitHub上注册一个OAuth App------这相当于你在GitHub那里给早报站开了一个"官方认证通道"。

📍 第1步:进入Developer settings

  1. 登录GitHub,点击右上角你的头像;
  2. 在下拉菜单中点击Settings(设置);
  3. 在左侧边栏一直往下拉,找到Developer settings(开发者设置)并点击。

💡 找不到? 直接在浏览器地址栏输入 https://github.com/settings/developers 也可以直达。

📍 第2步:进入OAuth Apps

在Developer settings页面,左侧边栏有三个选项,点击OAuth Apps 。

⚠️ 注意 :这里选 OAuth Apps ,不是GitHub Apps。

两者都基于OAuth 2.0,但GitHub Apps的权限模型更细粒度,配置也更复杂。

本专栏教学场景用OAuth Apps最简单、最直接。

📍 第3步:新建OAuth App

点击右上角的New OAuth App按钮。

💡 如果你从没创建过任何App :这个按钮会显示为 Register a new application(注册新应用)------意思完全一样,点它就行。

📍 第4步:填写应用信息(关键步骤)

这是最容易出错的一步,每个字段都要仔细填:

字段 填什么 说明
Application name Python早报(或你喜欢的名字) 用户授权时会看到这个名字
Homepage URL http://localhost:8000 应用的"主页地址"。开发环境就填本地地址
Application description Python早报站的GitHub登录 可选,用户授权时看到的描述
Authorization callback URL http://localhost:8000/auth/github/callback 🔴 最关键的一个字段------GitHub授权完成后会把用户"送回"这个地址

🔴 Authorization callback URL:为什么必须完全一致?

这个字段告诉GitHub:"用户同意授权后,把他重定向到我服务器的哪个地址"。

它必须和你代码里settings.github_redirect_uri的值逐字一致 ------端口、路径、localhost与127.0.0.1 的写法,差一个字符都会导致redirect_uri_mismatch错误。

⚠️ OAuth App只能设置一个回调URL ,不像GitHub Apps支持多个或通配符匹配。所以在开发阶段固定用http://localhost:8000/auth/github/callback,上线时再改。

📍 第5步:注册应用

确认所有字段无误后,点击底部的 Register application(注册应用)按钮。

📍 第6步:获取Client ID和Client Secret

注册成功后,页面会自动跳转到该应用的设置页。你会看到两个关键凭证:

凭证 在哪里 性质
Client ID 页面顶部直接显示 公开的,可以出现在前端代码里
Client Secret 点击Generate a new client secret按钮生成 🔴 机密,只显示一次,必须立即复制保存

🚨 Client Secret只显示一次! 如果你关掉页面才发现没复制,只能重新生成一个------旧的立即作废。把它写进.env文件,绝不进Git仓库。

📍 第7步:写入 .env

在项目根目录的.env文件里添加三行(如果.env不存在就新建,并确认它在.gitignore里):

text 复制代码
GITHUB_CLIENT_ID=你的Client_ID
GITHUB_CLIENT_SECRET=你的Client_Secret
GITHUB_REDIRECT_URI=http://localhost:8000/auth/github/callback

💡 三处对齐检查 :.env里的GITHUB_REDIRECT_URI、app/config.py里github_redirect_uri的默认值、以及GitHub OAuth App设置页里的 Authorization callback URL------这三处必须是同一个地址。


四、第1步:配置Settings

config.py加三个字段(.env自动注入):

python 复制代码
class Settings(BaseSettings):
    ...
    github_client_id: str = ""
    github_client_secret: str = ""
    github_redirect_uri: str = "http://localhost:8000/auth/github/callback"

五、第2步:发起登录------拼授权URL

app/routers/github.py:

python 复制代码
"""app/routers/github.py ------ GitHub OAuth登录"""
import secrets
import urllib.parse

import requests
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.config import settings
from app.deps import get_session
from core.models import User

router = APIRouter(prefix="/auth/github", tags=["github"])

GITHUB_AUTHORIZE = "https://github.com/login/oauth/authorize"
GITHUB_TOKEN = "https://github.com/login/oauth/access_token"
GITHUB_USER = "https://api.github.com/user"


@router.get("")
def github_login():
    """① 生成授权URL(演示版直接返回URL;生产用302 + state存会话)"""
    state = secrets.token_urlsafe(16)          # 防伪造回调的随机串
    params = {
        "client_id": settings.github_client_id,
        "redirect_uri": settings.github_redirect_uri,
        "scope": "read:user",                  # 只申请读用户信息
        "state": state,
    }
    url = f"{GITHUB_AUTHORIZE}?{urllib.parse.urlencode(params)}"
    return {"login_url": url}

💡 OAuth全程就是三次HTTP请求,没有任何新技术。


六、第3步:回调------换令牌、取用户、建用户、签JWT

先把create_token抽到共享模块(避免两处重复定义):

app/security.py(新建,从auth.py里抽出来):

python 复制代码
"""app/security.py ------ 令牌签发与校验(auth/github共用)"""
from datetime import datetime, timedelta, timezone

import jwt

from app.config import settings
from core.models import User


def create_token(user: User, expires_minutes: int, token_type: str) -> str:
    """签发令牌:payload只放身份标识和时间,不放敏感信息"""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": str(user.id),
        "type": token_type,
        "iat": now,
        "exp": now + timedelta(minutes=expires_minutes),
    }
    return jwt.encode(payload, settings.jwt_secret, algorithm="HS256")

💡 auth.py改为from app.security import create_token,删掉重复定义。

app/routers/github.py 继续:

python 复制代码
from app.security import create_token


@router.get("/callback")
def github_callback(
    code: str,
    state: str,
    session: Session = Depends(get_session),
):
    """③④⑤⑥ 回调四连:换token → 取用户 → find-or-create → 签自己的JWT"""
    # 演示版不校验state;完整校验见练习①

    # ④ code换access_token
    # ⚠️ GitHub的OAuth端点期望表单编码,必须用data=而不是json=
    token_resp = requests.post(GITHUB_TOKEN, headers={
        "Accept": "application/json",
    }, data={
        "client_id": settings.github_client_id,
        "client_secret": settings.github_client_secret,
        "code": code,
        "redirect_uri": settings.github_redirect_uri,
    }, timeout=10)
    access_token = token_resp.json().get("access_token")
    if not access_token:
        raise HTTPException(status_code=401, detail="GitHub授权失败,请重试")

    # ⑤ 取GitHub用户信息
    me = requests.get(GITHUB_USER, headers={
        "Authorization": f"Bearer {access_token}",
    }, timeout=10).json()

    # ⑥ find-or-create:按github_id找本地用户,没有就建
    user = session.scalar(select(User).where(
        User.github_id == str(me["id"])))
    if user is None:
        user = User(
            username=f"gh_{me['login']}",        # 前缀隔离,避免撞已有用户名
            github_id=str(me["id"]),
        )
        session.add(user)
        session.commit()

    # 签发自己的JWT,从此走第06篇的体系
    token = create_token(user, settings.jwt_expire_minutes, "access")
    return {"access_token": token, "token_type": "bearer",
            "username": user.username}

🔧 User模型加列(alembic第三次兑现)

python 复制代码
class User(Base):
    ...
    github_id: Mapped[str | None] = mapped_column(unique=True, default=None)
bash 复制代码
alembic revision --autogenerate -m "add user github_id"
alembic upgrade head

📌 挂载路由

app/main.py 补一行:

python 复制代码
from app.routers import articles, sources, auth, github

app.include_router(auth.router)
app.include_router(github.router)

七、验收清单

bash 复制代码
1. /docs → GET /auth/github → 复制login_url到浏览器
2. 跳转GitHub授权页 → 确认授权 → 自动跳回localhost回调地址
3. 回调返回JSON:access_token + 新用户名gh_xxx
4. 用这个token调/auth/me → 能查到用户(JWT体系无缝衔接)
5. psql看users表:github_id已存,password_hash为空(OAuth用户无密码,正常)
6. 再次走全流程 → 返回同一个用户(find-or-create生效,不重复建号)
7. 全程无报错后提交Git
bash 复制代码
git add .
git commit -m "GitHub OAuth登录:授权码流程 + find-or-create"

八、常见报错:这6个,OAuth的标配(重点!)

① 跳转授权页报redirect_uri_mismatch

🔍 原因 :注册回调地址与代码里的redirect_uri不完全一致------端口、路径、尾部斜杠差一个字符都不行。

✅ 解法 :两处逐字对齐。localhost与127.0.0.1也视为不同。


② 授权后回调报401或GitHub返回bad_verification_code

🔍 原因 :code是一次性的------刷新页面重复请求回调,code已作废。

✅ 解法:不是bug,重新发起登录即可。

📌 调试时记得每次走完整流程。


③ error=invalid_client_id / token换不到

🔍 原因:Client ID抄错,或Client Secret不是本App的(或.env没生效)。

✅ 解法 :回GitHub OAuth App页核对;重启uvicorn让.env重新加载(第05篇⑥)。


④ 用户名撞车:gh_xxx已存在,unique约束报错

🔍 原因:有人在你的站注册过同名用户,或重复建号竞态。

✅ 解法:

  • username冲突时追加github_id后缀(gh_xxx_12345);
  • 更稳的做法是用email做关联键(生产方案)。

⑤ 回调地址用127.0.0.1时,GitHub提示无法访问

🔍 原因 :GitHub对OAuth回调的localhost白名单有特殊处理------注册和代码必须统一用localhost或统一用127.0.0.1,混用必mismatch。

✅ 解法 :统一为localhost:8000。


⑥ Client Secret泄露了

🔍 原因 :可能进过git、贴过群聊------它是你 "代表这个App发言"的凭证。

✅ 解法:GitHub OAuth App页直接点 "Regenerate client secret"换新,旧的立即作废。

📌 密钥可重置,前提是发现得早。


九、课后练习

# 练习 难度 提示
1 state完整校验:发起登录时把state存进短期缓存(或签进2分钟JWT),回调时比对 ⭐⭐⭐ 伪造回调就此失效
2 头像入库 :me["avatar_url"]存进User ⭐⭐ 加列 + 迁移 + find-or-create赋值
3 邮箱关联 :scope加user:email,调/user/emails拿主邮箱,用它做find-or-create的键 ⭐⭐⭐ 生产级方案
4(选做) 平台对比:看企业微信/飞书/微信开放平台的OAuth文档 ⭐⭐ 流程完全一样,只是换端点换字段------学会一个,全会

📦 配套代码

完整github.py、security.py与迁移脚本已上传Git(python_daily/):【gitee仓库地址】

相关推荐
小陈运维2 小时前
微WEB文件服务 Docker 部署指南
前端·docker·容器
lichenyang4532 小时前
从 Prettier 开始整理前端开发常用命令
前端
哦虎!2 小时前
【前端】HTML
前端·html
计算机编程指导师2 小时前
【计算机毕设选题】基于Hadoop的全球企业邮件安全合规数据可视化分析系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·数据分析·毕业设计·课程设计·企业邮件
FY@-@2 小时前
Shell 脚本编程:条件判断与循环
linux·运维·前端
Source.Liu2 小时前
【A11】拆分前端,实现多页面方案
前端·rust
SimonKing2 小时前
零大模型依赖!SQLite FTS5 + jieba 打造纯本地知识库搜索
后端·python·程序员
慢云智慧空间2 小时前
从智能终端到空间AI,慢云科技如何重新定义智慧人居?
人工智能·python·科技
逛逛GitHub2 小时前
最近逛 GitHub ,挖到 3 个给力的开源项目,第一个比较硬核。
github