Skip to content

用户系统

认证方案选择

2025 年 Next.js 生态中,主流认证方案对比:

方案类型优点缺点适用场景
Clerk托管服务开箱即用、UI 组件完善、MFA 支持付费、数据不在自己手里快速上线、团队小
Auth.js开源库免费、完全控制、80+ OAuth 提供商需要自己实现 UI定制需求高、预算有限
Supabase AuthBaaS与数据库集成、免费额度高绑定 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 })
}

下一步

用户系统完成后,集成支付功能。

继续:支付集成 →


← 返回技术实现 | 返回项目五

最近更新

基于 Apache 2.0 许可发布