Skip to content

认证系统

IAM 项目使用 Better-Auth 提供完整的用户认证和授权功能。

认证系统使用 Better-Auth,配置在 packages/auth/src/index.ts

import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { admin, jwt } from 'better-auth/plugins'
import { oauthProvider } from '@better-auth/oauth-provider'
import { redis } from '@IAM/redis'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: schema
}),
trustedOrigins: [env.CORS_ORIGIN],
emailAndPassword: {
enabled: true
},
advanced: {
defaultCookieAttributes: {
sameSite: 'none',
secure: true,
httpOnly: true
}
},
plugins: [
admin(),
jwt(),
oauthProvider({
loginPage: '/sign-in',
consentPage: '/consent'
})
],
// 配置存储在 Redis 中以实现"一键踢下线"
secondaryStorage: {
get: async (key) => {
return await redis.get(key)
},
set: async (key, value, ttl) => {
if (ttl) {
await redis.set(key, value, { ex: ttl })
} else {
await redis.set(key, value)
}
},
delete: async (key) => {
await redis.del(key)
}
}
})
  • database: 使用 Drizzle 适配器连接 PostgreSQL
  • trustedOrigins: 允许的跨域来源
  • emailAndPassword: 启用邮箱密码认证
  • advanced: Cookie 安全配置
  • plugins: 通过插件扩展能力
    • admin(): 提供后台管理与更多模型
    • jwt(): JWT Token 支持
    • oauthProvider(): OAuth 2.1 / OIDC Provider 功能,详见 OAuth Provider 指南
  • secondaryStorage: 使用 Redis 存储会话数据,支持”一键踢下线”功能

项目使用 Upstash Redis 作为 secondaryStorage,用于存储会话数据和组织信息。这提供了以下优势:

  • 一键踢下线: 可以快速使所有用户会话失效
  • 高性能: Redis 提供快速的读写性能
  • 可扩展: 支持分布式部署

确保在环境变量中配置了 Redis 连接信息:

Terminal window
KV_REST_API_URL=https://your-redis-instance.upstash.io
KV_REST_API_TOKEN=your-redis-token

Redis 客户端在 packages/redis/src/index.ts 中配置,并在 packages/auth/src/index.ts 中使用。

启用认证插件后的数据库更新流程

Section titled “启用认证插件后的数据库更新流程”

当你调整 Better Auth 的插件组合、用户附加字段或 OAuth/OIDC 配置时,需要同步更新数据库结构。

当前项目主要使用:

import { admin, jwt } from 'better-auth/plugins'
export const auth = betterAuth({
// ...database、trustedOrigins、emailAndPassword、advanced 等配置
plugins: [admin(), jwt(), oauthProvider(/* ... */)]
})

以上配置变化可能会影响认证相关表结构,因此需要按以下步骤更新数据库:

  1. 根据最新配置生成/更新 Drizzle Schema

在仓库根目录执行:

Terminal window
pnpm dlx @better-auth/cli@latest generate \
--config packages/auth/src/index.ts \
--output packages/db/src/schema/auth.ts
  • --config:告诉 CLI 配置文件在 packages/auth/src/index.ts
  • --output:将根据插件生成的最新 Schema 输出到 packages/db/src/schema/auth.ts
  • 运行前请在根目录的 .env 中配置好以下环境变量:
    • DATABASE_URL
    • BETTER_AUTH_SECRET
    • BETTER_AUTH_URL
    • CORS_ORIGIN
  1. 生成数据库迁移文件
Terminal window
pnpm db:generate

该命令会通过 Turbo 在 @IAM/db 包中执行 drizzle-kit generate,在 packages/db/src/migrations 目录下生成迁移文件并更新 meta/_journal.json

  1. 应用迁移到数据库
Terminal window
pnpm db:migrate

该命令会在 @IAM/db 包中执行 drizzle-kit migrate,真正把上述变更应用到 DATABASE_URL 所指向的数据库中。

提示:在开发环境中,如果你临时使用 pnpm db:push 直接推送结构变更,且遇到终端里方向键或回车无法响应确认提示的情况,通常是因为根目录脚本会先进入 turbo -F @IAM/db db:push 的任务交互界面,而内部的 drizzle-kit push 又会继续弹出数据删除确认。两层交互叠加后,在 IDE 集成终端中偶尔会出现按键没有正确透传的问题。

这时建议直接绕过 Turbo,执行包内命令:

Terminal window
pnpm --filter @IAM/db run db:push

如果你已经明确确认要接受数据删除提示,也可以显式自动确认:

Terminal window
pnpm --filter @IAM/db run db:push --force

--force 会自动接受所有 data-loss 提示,只建议在你清楚知道会删除哪些表、字段或数据时使用。

小结:启用/调整 Better-Auth 插件后,一定要先用 CLI 重新生成 Schema,然后通过 db:generate / db:migrate 更新数据库结构。

在 React 组件中使用认证客户端:

import { authClient } from '@/lib/auth-client'
// 注册
await authClient.signUp.email({
email: 'user@example.com',
password: 'password123',
name: 'User Name'
})
// 登录
await authClient.signIn.email({
email: 'user@example.com',
password: 'password123'
})
// 登出
await authClient.signOut()

认证路由在 apps/server/src/index.ts 中配置:

app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw))

所有 /api/auth/* 路径的请求都会被 Better-Auth 处理。

Better-Auth 自动管理用户会话:

  • HTTP-only cookies: 使用安全的 HTTP-only cookies 存储会话
  • 跨域支持: 支持跨域请求(CORS 配置)
  • 自动刷新: 自动处理会话刷新
import { authClient } from '@/lib/auth-client'
const session = await authClient.getSession()
if (session) {
console.log('User:', session.user)
}

在 tRPC 上下文中:

packages/api/src/context.ts
export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: context.req.raw.headers
})
return { session }
}

在 Next.js 中使用服务器组件检查会话:

import { auth } from "@IAM/auth";
import { redirect } from "next/navigation";
import { headers } from "next/headers";
export default async function ProtectedPage() {
const session = await auth.api.getSession({
headers: headers(),
});
if (!session) {
redirect("/login");
}
return <div>Protected Content</div>;
}

使用 protectedProcedure 创建受保护的过程:

import { protectedProcedure } from '@IAM/api'
export const appRouter = router({
privateData: protectedProcedure.query(({ ctx }) => {
// ctx.session 包含当前用户会话
return {
message: 'This is private',
user: ctx.session.user
}
})
})

在受保护的过程中检查用户权限:

protectedProcedure.query(({ ctx }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Authentication required'
})
}
// 使用 ctx.session.user 访问用户信息
return { userId: ctx.session.user.id }
})
  1. 用户在前端填写注册表单
  2. 调用 authClient.signUp.email()
  3. Better-Auth 验证输入并创建用户
  4. 自动创建会话并设置 cookie
  5. 用户被重定向到受保护页面
  1. 用户在前端填写登录表单
  2. 调用 authClient.signIn.email()
  3. Better-Auth 验证凭据
  4. 创建会话并设置 cookie
  5. 用户被重定向到受保护页面
  1. 用户点击登出按钮
  2. 调用 authClient.signOut()
  3. Better-Auth 清除会话
  4. 用户被重定向到登录页面
  1. 密码强度: 确保密码符合安全要求
  2. HTTPS: 在生产环境使用 HTTPS
  3. Cookie 安全: 使用 secure 和 httpOnly cookies
  4. CORS 配置: 正确配置允许的来源
  5. 会话过期: 设置合理的会话过期时间

apps/web/src/lib/auth-client.ts 中配置认证客户端:

import { oauthProviderClient } from '@better-auth/oauth-provider/client'
import { createAuthClient } from 'better-auth/react'
import { adminClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
baseURL: env.NEXT_PUBLIC_SERVER_URL,
plugins: [adminClient(), oauthProviderClient()]
})
  • adminClient(): 管理端 API(用户管理、会话管理等)
  • oauthProviderClient(): OAuth Provider API(客户端管理、授权管理等)

当 IAM 系统作为 OAuth Provider 时,授权码流程的核心链路如下:

  1. 第三方客户端将用户重定向到 IAM 的授权端点
  2. 若用户未登录,IAM 先引导用户完成本地登录,再恢复 OIDC 流程
  3. 用户确认授权(或自动跳过),IAM 签发 authorization_code
  4. 第三方客户端用 code 在后端换取 access_token

完整流程详解(含时序图、每一步的实现细节和常见误区)请参阅 OAuth Provider 授权流程