身份验证
使用 Better Auth 进行用户身份验证 - 注册、登录、OAuth 和会话管理
身份验证
ProductReady 使用 Better Auth 实现现代化、安全的身份验证。本指南涵盖了从基础的邮箱/密码登录到 OAuth 集成的所有内容。
为什么选择 Better Auth? 现代化、类型安全、框架无关,且积极维护。比 NextAuth/Auth.js 简单得多。
快速概览
支持的认证方式:
- ✅ 邮箱 + 密码
- ✅ OAuth(GitHub、Google)
- ✅ 魔术链接(仅邮箱登录)- 可配置
- ✅ 会话管理
- ✅ 邮箱验证 - 可选
工作原理:
- 用户注册/登录
- Better Auth 创建会话
- 会话存储在数据库和 Cookie 中
- tRPC 检查受保护路由的会话
注册与登录
邮箱 + 密码流程
用户可以使用邮箱和密码创建账户。
注册页面:/sign-up(或自定义位置)
示例组件:
'use client';
import { authClient } from '~/lib/auth.client';
import { useState } from 'react';
export function SignUpForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
try {
await authClient.signUp.email({
email,
password,
name,
});
// 重定向到控制面板
window.location.href = '/dashboard';
} catch (error) {
console.error('注册失败:', error);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="姓名"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">注册</button>
</form>
);
}登录 类似 - 使用 authClient.signIn.email():
await authClient.signIn.email({
email,
password,
});OAuth(社交登录)
GitHub OAuth
让用户使用 GitHub 登录。
步骤 1:创建 GitHub OAuth 应用
- 前往 GitHub 设置 → 开发者设置
- 点击 "新建 OAuth 应用"
- 填写信息:
- 应用名称:你的应用名称
- 主页 URL:
http://localhost:3000(开发环境)或https://yourapp.com(生产环境) - 授权回调 URL:
http://localhost:3000/api/auth/callback/github
- 点击 "注册应用"
- 复制 Client ID 和 Client Secret
步骤 2:添加环境变量
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret步骤 3:在应用中使用
'use client';
import { authClient } from '~/lib/auth.client';
export function GitHubButton() {
function handleGitHubLogin() {
authClient.signIn.social({
provider: 'github',
callbackURL: '/dashboard',
});
}
return (
<button onClick={handleGitHubLogin}>
<GitHubIcon /> 使用 GitHub 登录
</button>
);
}Google OAuth
与 GitHub 类似:
步骤 1:创建 Google OAuth 凭据
- 前往 Google Cloud Console
- 创建项目(或选择现有项目)
- 启用 Google+ API
- 前往 凭据 → 创建凭据 → OAuth 客户端 ID
- 选择 Web 应用
- 添加授权重定向 URI:
http://localhost:3000/api/auth/callback/google - 复制 Client ID 和 Client Secret
步骤 2:环境变量
GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_google_client_secret步骤 3:在应用中使用
authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});会话管理
检查用户是否已登录
服务端(tRPC、API 路由):
import { auth } from '~/lib/auth';
import { headers } from 'next/headers';
export async function GET() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return new Response('未授权', { status: 401 });
}
return Response.json({ user: session.user });
}客户端(React 组件):
'use client';
import { authClient } from '~/lib/auth.client';
import { useEffect, useState } from 'react';
export function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
authClient.getSession().then((session) => {
if (session) {
setUser(session.user);
}
});
}, []);
if (!user) return <div>未登录</div>;
return <div>欢迎,{user.name}!</div>;
}登出
'use client';
import { authClient } from '~/lib/auth.client';
export function LogoutButton() {
async function handleLogout() {
await authClient.signOut();
window.location.href = '/';
}
return <button onClick={handleLogout}>登出</button>;
}受保护的路由(tRPC)
使用 protectedProcedure 要求身份验证:
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const usersRouter = createTRPCRouter({
me: protectedProcedure.query(async ({ ctx }) => {
// ctx.session.user 保证存在
return ctx.session.user;
}),
updateProfile: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
await ctx.db
.update(users)
.set({ name: input.name })
.where(eq(users.id, userId));
return { success: true };
}),
});工作原理:
protectedProcedure检查有效会话- 如果没有会话 → 抛出错误(前端显示登录提示)
- 如果会话存在 → 在处理程序中可以访问
ctx.session.user
中间件(页面保护)
使用中间件保护整个页面:
创建 src/middleware.ts:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { auth } from '~/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 保护 /dashboard 路由
if (pathname.startsWith('/dashboard')) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', pathname);
return NextResponse.redirect(url);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};现在所有 /dashboard/* 路由都需要登录!
邮箱验证(可选)
为新注册用户启用邮箱验证:
步骤 1:配置邮件提供商
ProductReady 支持任何邮件提供商。以 Resend 为例:
pnpm add resend在 src/lib/auth.ts 中配置:
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: true, // ← 启用验证
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await resend.emails.send({
from: 'noreply@yourapp.com',
to: user.email,
subject: '验证你的邮箱',
html: `点击验证:<a href="${url}">${url}</a>`,
});
},
},
});步骤 2:处理验证回调
Better Auth 会自动处理 /api/auth/verify-email?token=xxx 的回调。
用户点击链接 → 邮箱已验证 → 可以登录!
密码重置
步骤 1:请求重置
'use client';
import { authClient } from '~/lib/auth.client';
export function ForgotPasswordForm() {
const [email, setEmail] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
await authClient.forgetPassword({
email,
redirectTo: '/reset-password',
});
alert('请查看你的邮箱获取重置链接!');
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="你的邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">发送重置链接</button>
</form>
);
}步骤 2:重置密码页面
创建 /reset-password/page.tsx:
'use client';
import { authClient } from '~/lib/auth.client';
import { useSearchParams } from 'next/navigation';
export default function ResetPasswordPage() {
const searchParams = useSearchParams();
const token = searchParams.get('token');
const [password, setPassword] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
await authClient.resetPassword({
newPassword: password,
token: token!,
});
alert('密码已重置!你现在可以登录了。');
window.location.href = '/login';
}
return (
<form onSubmit={handleSubmit}>
<input
type="password"
placeholder="新密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">重置密码</button>
</form>
);
}用户管理
获取当前用户(tRPC)
export const usersRouter = createTRPCRouter({
me: protectedProcedure.query(async ({ ctx }) => {
return ctx.session.user;
}),
});从前端调用:
const { data: user } = trpc.users.me.useQuery();更新用户资料
export const usersRouter = createTRPCRouter({
updateProfile: protectedProcedure
.input(z.object({
name: z.string().optional(),
image: z.string().url().optional(),
}))
.mutation(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
await ctx.db
.update(users)
.set(input)
.where(eq(users.id, userId));
return { success: true };
}),
});删除账户
export const usersRouter = createTRPCRouter({
deleteAccount: protectedProcedure.mutation(async ({ ctx }) => {
const userId = ctx.session.user.id;
// 删除用户数据(级联删除相关记录)
await ctx.db
.delete(users)
.where(eq(users.id, userId));
// 登出
await authClient.signOut();
return { success: true };
}),
});最佳实践
✅ 应该做的
- 哈希密码 - Better Auth 会自动完成
- 在生产环境使用 HTTPS - 安全 Cookie 所必需
- 设置安全的会话过期时间 - 默认 7 天,可配置
- 验证邮箱格式 - 使用 Zod schema 检查格式
- 限制认证端点速率 - 防止暴力破解攻击
// 速率限制示例(使用 Upstash 或类似服务)
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 每分钟 5 次尝试
});❌ 不应该做的
- 不要以明文存储密码 - 始终使用哈希(Better Auth 会处理)
- 不要暴露用户令牌 - 仅保留在服务端
- 不要跳过邮箱验证 - 使用正确的正则表达式或库
- 不要允许弱密码 - 强制最小长度(8+ 字符)
安全检查清单
-
BETTER_AUTH_SECRET是随机且安全的(32+ 字符) - 开发环境和生产环境使用不同的密钥
- 生产环境启用 HTTPS
- 会话 Cookie 设置为
httpOnly和secure - 强制密码最小长度(8+ 字符)
- 认证端点启用速率限制
- 邮箱验证已启用(如果需要)
- OAuth 回调 URL 完全匹配
- CSRF 保护已启用(Better Auth 默认)
故障排除
受保护路由显示"未授权"
检查:
- 你已登录了吗?使用
authClient.getSession()测试 - Cookie 正在发送吗?查看浏览器开发者工具 → 应用 → Cookies
BETTER_AUTH_URL正确吗?应该与你的域名完全匹配
OAuth 无法工作
检查:
.env中的 Client ID/Secret(无引号,无空格)- 回调 URL 完全匹配(包括 http:// 或 https://)
- 更改环境变量后重新部署(Vercel)
会话过期太快
在 src/lib/auth.ts 中延长会话持续时间:
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 天(以秒为单位)
},
});"无法读取 user 属性(undefined)"
会话尚未加载 - 添加加载状态:
const [session, setSession] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
authClient.getSession().then((s) => {
setSession(s);
setLoading(false);
});
}, []);
if (loading) return <div>加载中...</div>;
if (!session) return <div>未登录</div>;高级:自定义认证 UI
使用 Better Auth 原语构建你自己的认证组件:
'use client';
import { authClient } from '~/lib/auth.client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function CustomLoginForm() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setLoading(true);
try {
const result = await authClient.signIn.email({
email,
password,
});
if (result.error) {
setError(result.error.message);
return;
}
router.push('/dashboard');
} catch (err) {
setError('登录失败。请重试。');
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && <div className="error">{error}</div>}
<input
type="email"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
);
}下一步
📚 更多资源: