用户系统
认证方案选择
2025 年 Next.js 生态中,主流认证方案对比:
| 方案 | 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Clerk | 托管服务 | 开箱即用、UI 组件完善、MFA 支持 | 付费、数据不在自己手里 | 快速上线、团队小 |
| Auth.js | 开源库 | 免费、完全控制、80+ OAuth 提供商 | 需要自己实现 UI | 定制需求高、预算有限 |
| Supabase Auth | BaaS | 与数据库集成、免费额度高 | 绑定 Supabase 生态 | 已使用 Supabase |
根据 Clerk 的 2025 年报告,38% 的数据泄露是通过凭证窃取发生的。认证是你最重要的安全决策。
方案一:Clerk(推荐快速上线)
Clerk 提供完整的用户管理解决方案,包括预构建的 UI 组件。
安装配置
bash
pnpm add @clerk/nextjs配置环境变量(.env.local):
env
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up中间件配置
创建 middleware.ts:
typescript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/pricing',
'/api/webhooks(.*)'
])
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect()
}
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}使用认证组件
tsx
// app/(auth)/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignIn />
</div>
)
}获取用户信息
tsx
// Server Component
import { currentUser } from '@clerk/nextjs/server'
export default async function DashboardPage() {
const user = await currentUser()
return <div>欢迎, {user?.firstName}</div>
}tsx
// Client Component
'use client'
import { useUser } from '@clerk/nextjs'
export function UserProfile() {
const { user, isLoaded } = useUser()
if (!isLoaded) return <div>加载中...</div>
return <div>欢迎, {user?.firstName}</div>
}方案二:Auth.js(推荐完全控制)
Auth.js(原 NextAuth.js)是开源认证库,完全免费。
安装配置
bash
pnpm add next-auth@beta @auth/prisma-adapter配置 Auth.js
创建 lib/auth.ts:
typescript
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { db } from './db'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(db),
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
pages: {
signIn: '/login',
},
callbacks: {
session({ session, user }) {
session.user.id = user.id
return session
},
},
})API 路由
typescript
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/lib/auth'
export const { GET, POST } = handlers中间件
typescript
// middleware.ts
import { auth } from '@/lib/auth'
export default auth((req) => {
const isLoggedIn = !!req.auth
const isAuthPage = req.nextUrl.pathname.startsWith('/login')
const isProtectedRoute = req.nextUrl.pathname.startsWith('/dashboard')
if (isProtectedRoute && !isLoggedIn) {
return Response.redirect(new URL('/login', req.nextUrl))
}
if (isAuthPage && isLoggedIn) {
return Response.redirect(new URL('/dashboard', req.nextUrl))
}
})
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}登录页面
tsx
// app/(auth)/login/page.tsx
import { signIn } from '@/lib/auth'
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-4 p-8">
<h1 className="text-2xl font-bold text-center">登录</h1>
<form
action={async () => {
'use server'
await signIn('github')
}}
>
<button className="w-full bg-black text-white py-2 rounded">
使用 GitHub 登录
</button>
</form>
<form
action={async () => {
'use server'
await signIn('google')
}}
>
<button className="w-full bg-blue-500 text-white py-2 rounded">
使用 Google 登录
</button>
</form>
</div>
</div>
)
}权限控制
基于角色的访问控制(RBAC)
扩展数据库 schema:
prisma
model User {
id String @id @default(cuid())
email String @unique
role Role @default(USER)
// ...
}
enum Role {
USER
ADMIN
SUPER_ADMIN
}权限检查
typescript
// lib/auth.ts
export async function requireAdmin() {
const session = await auth()
if (!session?.user) {
throw new Error('未登录')
}
const user = await db.user.findUnique({
where: { id: session.user.id }
})
if (user?.role !== 'ADMIN') {
throw new Error('无权限')
}
return user
}在页面中使用
tsx
// app/admin/page.tsx
import { requireAdmin } from '@/lib/auth'
export default async function AdminPage() {
const admin = await requireAdmin()
return <div>管理员面板</div>
}安全最佳实践
1. 会话管理
- 设置合理的会话过期时间
- 支持"记住我"功能
- 敏感操作要求重新认证
2. 密码策略(如果使用密码登录)
- 最少 8 个字符
- 包含大小写字母和数字
- 使用 bcrypt 或 argon2 加密
3. 多因素认证(MFA)
Clerk 内置 MFA 支持。Auth.js 需要自己实现或使用第三方库。
4. 速率限制
防止暴力破解:
typescript
// 使用 upstash/ratelimit
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 每分钟 5 次
})
// 在登录 API 中使用
const { success } = await ratelimit.limit(ip)
if (!success) {
return new Response('请求过于频繁', { status: 429 })
}下一步
用户系统完成后,集成支付功能。