认证系统
IAM 使用 Better Auth 1.7.1 提供邮箱密码、企业微信登录、会话和 OAuth/OIDC Provider 能力。
Better-Auth 配置
Section titled “Better-Auth 配置”认证系统使用 Better-Auth,配置在 packages/auth/src/index.ts:
import { betterAuth } from 'better-auth'import { drizzleAdapter } from 'better-auth/adapters/drizzle'import { admin, genericOAuth, jwt } from 'better-auth/plugins'import { oauthProvider } from '@better-auth/oauth-provider'import { authRateLimitConfig } from './rate-limit'
export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg', schema: schema }), trustedOrigins: [env.CORS_ORIGIN], emailAndPassword: { enabled: true, disableSignUp: true, requireEmailVerification: true, minPasswordLength: 8, maxPasswordLength: 64, resetPasswordTokenExpiresIn: 60 * 60, revokeSessionsOnPasswordReset: true, sendResetPassword: async ({ user, url }) => { await sendAuthEmail(/* ... */) } }, emailVerification: { sendOnSignUp: false, sendOnSignIn: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { await sendAuthEmail(/* ... */) } }, advanced: { ipAddress: { ipAddressHeaders: ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for'] } }, rateLimit: authRateLimitConfig, plugins: [ admin(), jwt(), genericOAuth({ config: [wecomProvider(/* ... */)] }), oauthProvider({ /* loginPage、consentPage、claims */ }) ]})- database: 使用 Drizzle 适配器连接 PostgreSQL
- trustedOrigins: 允许的跨域来源
- emailAndPassword: 启用邮箱密码作为企微开户后的备用登录;禁止公开注册;未验证邮箱不能建立密码登录会话
- emailVerification: 未验证用户尝试密码登录时补发 24 小时有效的验证邮件
- advanced.ipAddress: 在 Cloudflare Workers 上读取真实客户端 IP,供限流使用
- rateLimit: 使用 Postgres
rate_limit表做原子计数,覆盖登录、注册、重置密码和 OAuth token/revoke/introspect - plugins: 通过插件扩展能力
admin(): 提供后台管理与更多模型jwt(): JWT Token 支持genericOAuth(): 当前仅注册企业微信wecomoauthProvider(): OAuth 2.1 / OIDC Provider 功能,详见 OAuth Provider 指南
认证邮件由 packages/auth/src/email.ts 统一发送。生产通过 Cloudflare EMAIL Binding;本地没有 Binding 时把邮件内容输出到服务端日志。企业微信占位邮箱(@wecom.invalid / @wecom.com)不可投递,会被跳过。
Cloudflare Workers 是多实例环境,Better Auth 默认的内存限流无法全局共享计数。当前实现用单条 SQL UPSERT 写入 rate_limit 表,窗口过期后重置计数。
不要把 Better Auth secondaryStorage 接到 Redis:1.7 的 secondary storage 还会承载 session、verification,启用后会改变存储语义。会话仍保存在 Postgres。
启用认证插件后的数据库更新流程
Section titled “启用认证插件后的数据库更新流程”当你调整 Better Auth 的插件组合、用户附加字段或 OAuth/OIDC 配置时,需要同步更新数据库结构。
当前项目主要使用:
import { admin, genericOAuth, jwt } from 'better-auth/plugins'
export const auth = betterAuth({ // ...database、trustedOrigins、emailAndPassword、advanced 等配置 plugins: [admin(), jwt(), genericOAuth(/* ... */), oauthProvider(/* ... */)]})以上配置变化可能会影响认证相关表结构,因此需要按以下步骤更新数据库:
- 根据最新配置生成/更新 Drizzle Schema
在仓库根目录执行:
pnpm dlx auth@1.7.1 generate --yes \ --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_URLBETTER_AUTH_SECRETBETTER_AUTH_URLCORS_ORIGIN
- 生成数据库迁移文件
pnpm db:generate该命令会通过 Turbo 在 @IAM/db 包中执行 drizzle-kit generate,在 packages/db/src/migrations 目录下生成迁移文件并更新 meta/_journal.json。
- 应用迁移到数据库
pnpm db:migrate该命令会在 @IAM/db 包中执行 drizzle-kit migrate,真正把上述变更应用到 DATABASE_URL 所指向的数据库中。
db:push只用于可丢弃的本地数据库临时验证。正式变更必须生成并提交 migration。
小结:启用/调整 Better-Auth 插件后,一定要先用 CLI 重新生成 Schema,然后通过
db:generate/db:migrate更新数据库结构。
用户注册和登录
Section titled “用户注册和登录”在 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 配置)
- 自动刷新: 自动处理会话刷新
获取当前会话
Section titled “获取当前会话”import { authClient } from '@/lib/auth-client'
const session = await authClient.getSession()if (session) { console.log('User:', session.user)}在 tRPC 上下文中:
export async function createContext({ context }: CreateContextOptions) { const session = await auth.api.getSession({ headers: context.req.raw.headers }) return { session }}保护路由和 API
Section titled “保护路由和 API”保护前端路由
Section titled “保护前端路由”apps/web 不直接导入 @IAM/auth。当前项目使用 apps/web/src/proxy.ts,通过同源 get-session 请求和 AUTH_API Service Binding 保护 /admin、/profile、/apps 和资料补全页面,并根据平台角色做管理端白名单判断。
保护 tRPC 过程
Section titled “保护 tRPC 过程”使用 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 } })})检查用户权限
Section titled “检查用户权限”在受保护的过程中检查用户权限:
protectedProcedure.query(({ ctx }) => { if (!ctx.session) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Authentication required' }) }
// 使用 ctx.session.user 访问用户信息 return { userId: ctx.session.user.id }})- 用户在前端填写注册表单
- 调用
authClient.signUp.email() - Better-Auth 验证输入并创建用户
- 自动创建会话并设置 cookie
- 用户被重定向到受保护页面
注册时会发送邮箱验证邮件。未验证邮箱的用户会被引导到完善资料页完成验证;验证通过前不会向 CRM 等下游系统签发可用于首次绑定的邮箱 claims。
密码重置流程
Section titled “密码重置流程”- 用户在
/forgot-password提交邮箱 - Better Auth 生成 1 小时有效的重置链接
- 用户在
/reset-password设置至少 8 位的新密码 - 密码更新成功后,旧会话自动失效
为防止账号枚举,无论邮箱是否存在,请求接口都会返回相同的成功提示。企微占位邮箱
(@wecom.invalid / @wecom.com)不可接收重置邮件;用户需先通过企业微信登录并完善真实邮箱。
生产环境通过 Cloudflare Email Binding 发送邮件,发送任务使用 Worker waitUntil 执行,并在日志中记录
requestId、Cloudflare messageId、收件域名和主题,便于排查投递问题且不记录完整收件地址。
- 用户在前端填写登录表单
- 调用
authClient.signIn.email() - Better-Auth 验证凭据
- 创建会话并设置 cookie
- 用户被重定向到受保护页面
- 用户点击登出按钮
- 调用
authClient.signOut() - Better-Auth 清除会话
- 用户被重定向到登录页面
企业微信首次登录与下游绑定
Section titled “企业微信首次登录与下游绑定”企业微信只提供 CorpID + UserID,不把通讯录邮箱当作可信绑定依据。首次登录必须走完善资料页:
- 用户使用企业微信登录,IAM 以 CorpID + UserID 建立本地账号
- IAM 要求填写可投递的企业邮箱并设置密码
- 发送验证邮件;验证成功前不能续接 OAuth
- 验证通过后,ID Token / UserInfo 返回
email与email_verified=true - CRM 等业务系统仅在
email_verified === true时按邮箱做一次首次绑定,并立即写入iam_subject(即 IAMsub) - 后续登录只按
iam_subject识别用户,邮箱变更不再导致账号漂移
IAM 身份主键始终是用户 id(OIDC sub),不是邮箱。占位地址 @wecom.invalid / @wecom.com 不会出现在 OAuth claims 中。
安全最佳实践
Section titled “安全最佳实践”- 密码强度: 确保密码符合安全要求
- HTTPS: 在生产环境使用 HTTPS
- Cookie 安全: 使用 secure 和 httpOnly cookies
- CORS 配置: 正确配置允许的来源
- 会话过期: 设置合理的会话过期时间
前端 Auth Client 配置
Section titled “前端 Auth Client 配置”在 apps/web/src/lib/auth-client.ts 中配置认证客户端:
import { oauthProviderClient } from '@better-auth/oauth-provider/client'import { createAuthClient } from 'better-auth/react'import { adminClient, customSessionClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({ // 省略 baseURL,浏览器同源访问 https://iam.cq-i.cn/api/auth。 // Web Worker 会将 /api/* 代理到 Hono 内部上游。 plugins: [adminClient(), oauthProviderClient(), customSessionClient()]})客户端插件说明
Section titled “客户端插件说明”adminClient(): 管理端 API(用户管理、会话管理等)oauthProviderClient(): OAuth Provider API(客户端管理、授权管理等)customSessionClient(): 推断自定义 session 字段,例如isAdmin与profileCompleted
平台角色与应用角色
Section titled “平台角色与应用角色”- 平台角色为
admin/user,控制 IAM 控制台权限。 BOOTSTRAP_ADMIN_WECOM_USER_IDS非空时,是企微用户平台角色的权威白名单;名单外企微用户会降为user。- OAuth 客户端可以独立定义应用角色,并按用户或部门分配。
- 应用角色通过 ID Token/UserInfo 的
app_role下发,不等同于平台管理员角色。
OAuth 2.1 授权码流程
Section titled “OAuth 2.1 授权码流程”当 IAM 系统作为 OAuth Provider 时,授权码流程的核心链路如下:
- 第三方客户端将用户重定向到 IAM 的授权端点
- 若用户未登录,IAM 先引导用户完成本地登录,再恢复 OIDC 流程
- 用户确认授权(或自动跳过),IAM 签发
authorization_code - 第三方客户端用
code在后端换取access_token
完整流程详解(含时序图、每一步的实现细节和常见误区)请参阅 OAuth Provider 授权流程。
- 了解 OAuth Provider 配置
- 了解 API 开发
- 查看 数据库管理
- 学习 开发工作流