关于 betterauth-fastapi-jwt-bridge
为 Better Auth(Next.js)与 FastAPI 提供 JWT 身份验证集成指导,使用 JWKS 验证令牌并保护 API 路由。还涵盖 UUID 用户标识模式、前后端集成和常见问题排查。
主要功能
- 01通过 JWKS 验证 JWT
- 02配置 FastAPI 身份验证依赖
- 03校验用户标识与路由权限
- 04支持 JWT 自定义 UUID 声明
- 05提供验证脚本和集成模板
适用场景
- 01将 Better Auth 登录接入 FastAPI
- 02为 API 路由添加用户级访问控制
- 03排查 JWKS、令牌或 UUID 集成问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bilalmk/ todo_correct --skill betterauth-fastapi-jwt-bridge安装后在 Claude Code 中输入/betterauth-fastapi-jwt-bridge确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/api_client.tsauth_dependencies.pybetter_auth_migrations.pyjwt_verification.pyreferences/jwks-approach.mdsecurity-checklist.mdtroubleshooting.mdscripts/test_jwt_verification.pyverify_jwks.py详细介绍
Better Auth + FastAPI JWT 桥接
使用 JWKS 验证,在 Better Auth(Next.js)与 FastAPI 之间实现生产就绪的 JWT 身份验证,确保身份验证安全且无状态。
架构
User Login (Frontend)
↓
Better Auth → Issues JWT Token
↓
Frontend API Request → Authorization: Bearer <token>
↓
FastAPI Backend → Verifies JWT with JWKS → Returns filtered data
快速入门工作流
步骤 1:在 Better Auth 中启用 JWT(前端)
// lib/auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [jwt()], // Enables JWT + JWKS endpoint
// ... other config
})
需要执行数据库迁移:
添加 JWT 插件后,运行迁移以创建所需的表:
# Next.js (Better Auth CLI)
npx @better-auth/cli migrate
⚠️ 重要——需要两个独立的表:
-
session表必须包含token列(Better Auth 的核心要求)- 错误:
column "token" of relation "session" does not exist - 修复方法:参见数据库架构问题
- 错误:
-
jwks表必须存在(JWT 插件的要求)- 错误:
relation "jwks" does not exist - 修复方法:参见数据库架构问题
- 错误:
这是两个独立的迁移。JWT 插件会创建 jwks 表,但不会修改 session 表。
步骤 2:验证 JWKS 端点
测试 JWKS 端点是否正常工作:
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwks
步骤 3:实现后端验证
将 assets/ 中的模板复制到 FastAPI 项目:
assets/jwt_verification.py→backend/app/auth/jwt_verification.pyassets/auth_dependencies.py→backend/app/auth/dependencies.py
安装依赖:
pip install fastapi python-jose[cryptography] pyjwt cryptography httpx
步骤 4:保护 API 路由
from app.auth.dependencies import verify_user_access
@router.get("/{user_id}/tasks")
async def get_tasks(
user_id: str,
user: dict = Depends(verify_user_access)
):
# user_id is verified to match authenticated user
return get_user_tasks(user_id)
步骤 5:配置前端 API 客户端
将 assets/api_client.ts 复制到 frontend/lib/api-client.ts 并使用:
import { getTasks, createTask } from "@/lib/api-client"
const tasks = await getTasks(userId)
⚠️ React 组件模式:
Better Auth 不提供 useSession() 钩子。请使用 useEffect 配合 authClient.getSession():
import { useState, useEffect } from "react"
import { authClient } from "@/lib/auth-client"
function MyComponent() {
const [user, setUser] = useState(null)
useEffect(() => {
async function loadSession() {
const session = await authClient.getSession()
if (session?.data?.user) {
setUser(session.data.user)
}
}
loadSession()
}, [])
return <div>Welcome {user?.name}</div>
}
完整示例请参见前端集成问题。
Better Auth UUID 集成(混合 ID 架构)
解决的问题: Better Auth 内部使用 String ID,但应用通常需要 UUID,以确保 API 路由和数据库外键中的类型一致。
解决方案: 采用混合 ID 方案——User 表同时包含 id(String,Better Auth 要求)和 uuid(UUID,供应用使用)。
数据库架构
CREATE TABLE "user" (
id VARCHAR PRIMARY KEY, -- Better Auth String ID
uuid UUID UNIQUE NOT NULL, -- Application UUID ⭐
email VARCHAR UNIQUE NOT NULL,
"emailVerified" BOOLEAN DEFAULT FALSE,
name VARCHAR,
"createdAt" TIMESTAMP NOT NULL,
"updatedAt" TIMESTAMP NOT NULL
);
-- UUID auto-generated by database
ALTER TABLE "user" ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
-- All foreign keys point to user.uuid
CREATE TABLE tasks (
id UUID PRIMARY KEY,
user_id UUID REFERENCES "user"(uuid) ON DELETE CASCADE, -- ⭐ FK to uuid
title VARCHAR NOT NULL,
...
);
前端配置(Better Auth)
添加 UUID 生成钩子和 JWT 自定义声明:
// lib/auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
import { Pool } from "pg"
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
export const auth = betterAuth({
database: pool,
// Hook to fetch database-generated UUID
hooks: {
user: {
created: async ({ user }) => {
const result = await pool.query(
'SELECT uuid FROM "user" WHERE id = $1',
[user.id]
)
const uuid = result.rows[0]?.uuid
return { ...user, uuid }
}
}
},
// Include UUID in JWT payload
plugins: [
jwt({
algorithm: "EdDSA",
async jwt(user, session) {
return {
uuid: user.uuid, // ⭐ Custom claim for backend
}
},
}),
],
})
后端模式(FastAPI)
从 JWT 自定义声明(而非 sub)中提取 UUID:
# backend/app/auth/dependencies.py
from uuid import UUID
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
payload = verify_jwt_token(token)
# Extract UUID from custom claim (not 'sub')
user_uuid_str = payload.get("uuid") # ⭐
user_uuid = UUID(user_uuid_str)
# Query by UUID
user = await session.execute(
select(User).where(User.uuid == user_uuid)
)
return user.scalar_one_or_none()
async def verify_user_match(
user_id: UUID, # From URL path
current_user: User = Depends(get_current_user)
) -> User:
# Compare UUIDs (not String IDs)
if current_user.uuid != user_id:
raise HTTPException(403, "Not authorized")
return current_user
关键模式: 始终使用 User.uuid 查询,并验证它是否与 JWT 自定义声明中的 UUID 一致。
核心组件
1. JWKS 验证流程
- 从 Better Auth 端点获取 JWKS(带缓存)
- 从 JWT 令牌头部提取 kid(密钥 ID)
- 在 JWKS 中通过 kid 查找匹配的公钥
- 使用 Ed25519 公钥验证签名
- 验证声明(签发者、受众、过期时间)
- 从负载中提取用户信息(
sub声明)
2. 用户隔离模式
始终验证 JWT 中的 user_id 是否与 URL 中的 user_id 一致:
if current_user["user_id"] != user_id:
raise HTTPException(status_code=403, detail="Not authorized")
这样可以防止用户访问其他用户的数据。
3. JWT 负载结构(已更新,支持 UUID 集成)
{
"sub": "user_abc123", // Better Auth String ID
"uuid": "a1b2c3d4-e5f6...", // Application UUID (custom claim) ⭐
"email": "[email protected]",
"name": "User Name",
"iat": 1234567890, // Issued at
"exp": 1234567890, // Expiration
"iss": "http://localhost:3000",
"aud": "http://localhost:3000"
}
重要: uuid 自定义声明用于后端用户识别和数据库查询。
Better Auth 使用 String ID(sub)管理用户,而应用使用 UUID(uuid)确保类型一致。
环境配置
前端(.env.local):
BETTER_AUTH_SECRET="min-32-chars-secret"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:8000"
后端(.env):
BETTER_AUTH_URL="http://localhost:3000"
DATABASE_URL="postgresql://..."
测试与验证
测试 JWKS 端点
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwks
预期输出会显示包含 kid、kty、crv 和 x 字段的公钥。
测试 JWT 验证
python scripts/test_jwt_verification.py \
--jwks-url http://localhost:3000/api/auth/jwks \
--token "eyJhbGci..."
故障排除
身份验证问题
| 问题 | 解决方案 |
|---|---|
| "relation 'jwks' does not exist" | 创建 JWKS 表迁移 — 参见 数据库架构问题 |
| "column 'token' does not exist" | 将 token 列添加到会话表 — 参见 数据库架构问题 |
| "Token missing UUID (uuid claim)" | 配置 Better Auth hook 和 JWT 插件 — 参见 UUID 集成问题 |
| "User not found after registration" | 双重身份验证系统冲突 — 参见 UUID 集成问题 |
| "authClient.useSession is not a function" | 在 useEffect 中使用 authClient.getSession() — 参见 前端集成问题 |
| "No authentication token available" | 使用 session.data.session.token,而不是 session.session.token — 参见 前端集成问题 |
| "Unable to find matching signing key" | 在 jwt_verification.py 中清除 JWKS 缓存 |
| "Token has expired" | 前端需要刷新会话 |
| "Invalid token claims" | 检查签发者/受众是否与 BETTER_AUTH_URL 匹配 |
| 403 Forbidden(UUID 不匹配) | 确保比较的是 UUID,而不是 String 与 UUID — 参见 UUID 集成问题 |
前端-后端集成问题(新增 - 2026-01-02)
| 问题 | 根本原因 | 解决方案 |
|---|---|---|
| 尽管返回 200 OK,但任务未显示 | 后端返回数组,前端期望分页对象 | 使用 Array.isArray() 检查并处理两种格式 - 参见前端-后端集成 |
| 标签筛选导致崩溃 | 后端返回标签对象 {id, name, color},前端期望 number[] | 更新 TypeScript 类型,使其与 Pydantic 架构匹配 - 参见标签筛选 |
| 分页显示“NaN” | 可选的优先级字段未进行空值检查便参与运算 | 为可选字段添加空值检查和默认值 - 参见优先级排序 |
| 标签未保存到数据库 | TaskCreate 架构不接受 tags 字段 | 使用多步操作:先创建任务,再分配标签 - 参见标签分配 |
| 编辑表单字段为空白 | 非受控组件、字段名称不匹配以及日期时间格式问题 | 使用受控组件,匹配字段名称,并转换日期时间 - 参见编辑表单 |
| 500 错误:时区比较 | 比较不带时区偏移和带时区偏移的日期时间 | 比较前将两者都规范化为 UTC - 参见时区修复 |
| 标签颜色验证失败 | 前端要求颜色,后端允许颜色为可选项 | 在 Zod 架构中将颜色设为可选,并提供默认值 - 参见标签颜色 |
| 标签筛选复选框失效 | 后端返回 id: number,FilterContext 使用 string[] | 将 ID 转换为字符串以进行比较 - 参见标签筛选器 |
**📚 必读内容:**请参阅故障排除指南中的前端-后端集成问题部分,了解包含代码示例的详细修复方法。本部分记录了实现过程中发现的 8 个关键问题及其解决方法。
关键经验:
- 编写前端类型前,务必先阅读后端 Pydantic 架构
- 使用空值检查和默认值处理可选字段
- 预填表单应使用受控组件
- 确保前端和后端的字段名称完全一致
- 使用实际后端响应进行测试,而非模拟数据
详细解决方案和预防策略请参阅 references/troubleshooting.md。
高级主题
JWKS 缓存策略
此实现使用 @lru_cache 缓存 JWKS 响应:
- 如果令牌包含未知
kid,则清除缓存 - 公钥很少变更(可安全缓存)
- 减少对 Better Auth 的网络请求
实现详情请参阅 references/jwks-approach.md。
安全检查清单
投入生产前:
- ✅ 所有 API 调用仅使用 HTTPS
- ✅ 验证令牌是否过期
- ✅ 验证颁发者/受众声明
- ✅ 强制执行用户 ID 授权
- ✅ 正确配置 CORS
- ✅ 错误消息不会泄露敏感信息
完整清单请参阅 references/security-checklist.md。
资源
scripts/
verify_jwks.py- 测试 JWKS 端点是否可用test_jwt_verification.py- 验证 JWT 令牌校验
references/
jwks-approach.md- JWKS 实现详细指南security-checklist.md- 生产环境安全要求troubleshooting.md- 常见问题及修复方法
assets/
jwt_verification.py- 完整的 JWKS 验证模块模板auth_dependencies.py- FastAPI 依赖项模板api_client.ts- 前端 API 客户端模板better_auth_migrations.py- Better Auth 表的 Alembic 迁移模板(包括令牌列修复)
为什么选择 JWKS 而不是共享密钥?
| 方面 | JWKS | 共享密钥 |
|---|---|---|
| 安全性 | ✅ 非对称(更安全) | ⚠️ 对称(安全性较低) |
| 可扩展性 | ✅ 多个后端 | ⚠️ 必须共享密钥 |
| 生产环境 | ✅ 推荐 | ⚠️ 仅用于开发 |
| 复杂度 | 中等 | 简单 |
建议: 生产环境始终使用 JWKS。