nextjs-authentication

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.js Authentication

Next.js 认证方案

Overview

概述

This skill provides comprehensive authentication patterns for Next.js 15+ applications using the App Router architecture and Auth.js 5. It covers the complete authentication lifecycle from initial setup to production-ready implementations with role-based access control.
Key capabilities include:
  • Auth.js 5 setup with Next.js App Router
  • Protected routes using Middleware
  • Session management in Server Components
  • Authentication checks in Server Actions
  • OAuth provider integration (GitHub, Google, etc.)
  • Role-based access control (RBAC)
  • JWT and database session strategies
  • Comprehensive testing patterns
本方案为采用App Router架构的Next.js 15+应用提供全面的认证实现模式,涵盖从初始搭建到具备基于角色访问控制的生产级实现的完整认证生命周期。
核心功能包括:
  • 基于Next.js App Router的Auth.js 5搭建
  • 使用Middleware实现受保护路由
  • Server Components中的会话管理
  • Server Actions中的认证校验
  • OAuth提供商集成(GitHub、Google等)
  • 基于角色的访问控制(RBAC)
  • JWT与数据库会话策略
  • 全面的测试模式

When to Use

适用场景

Use this skill when implementing authentication for Next.js 15+ with App Router:
  • Setting up Auth.js 5 (NextAuth.js) from scratch
  • Implementing protected routes with Middleware
  • Handling authentication in Server Components
  • Securing Server Actions with auth checks
  • Configuring OAuth providers (Google, GitHub, Discord, etc.)
  • Implementing role-based access control (RBAC)
  • Managing sessions with JWT or database strategy
  • Creating credential-based authentication
  • Handling sign-in/sign-out flows
  • Testing authentication flows
在为采用App Router的Next.js 15+应用实现认证时使用本方案:
  • 从零开始搭建Auth.js 5(NextAuth.js)
  • 使用Middleware实现受保护路由
  • 在Server Components中处理认证逻辑
  • 通过认证校验保护Server Actions
  • 配置OAuth提供商(Google、GitHub、Discord等)
  • 实现基于角色的访问控制(RBAC)
  • 使用JWT或数据库策略管理会话
  • 创建基于凭证的认证机制
  • 处理登录/登出流程
  • 测试认证流程

Instructions

操作步骤

1. Install Dependencies

1. 安装依赖

Install Auth.js v5 (beta) for Next.js App Router:
bash
npm install next-auth@beta
为Next.js App Router安装Auth.js v5(测试版):
bash
npm install next-auth@beta

2. Configure Environment Variables

2. 配置环境变量

Create
.env.local
with required variables:
bash
undefined
创建
.env.local
文件并添加必要变量:
bash
undefined

Required for Auth.js

Auth.js 必需配置

AUTH_SECRET="your-secret-key-here" AUTH_URL="http://localhost:3000"
AUTH_SECRET="your-secret-key-here" AUTH_URL="http://localhost:3000"

OAuth Providers (add as needed)

OAuth 提供商(按需添加)

GITHUB_ID="your-github-client-id" GITHUB_SECRET="your-github-client-secret" GOOGLE_CLIENT_ID="your-google-client-id" GOOGLE_CLIENT_SECRET="your-google-client-secret"

Generate `AUTH_SECRET` with:
```bash
openssl rand -base64 32
GITHUB_ID="your-github-client-id" GITHUB_SECRET="your-github-client-secret" GOOGLE_CLIENT_ID="your-google-client-id" GOOGLE_CLIENT_SECRET="your-google-client-secret"

通过以下命令生成`AUTH_SECRET`:
```bash
openssl rand -base64 32

3. Create Auth Configuration

3. 创建认证配置

Create
auth.ts
in the project root with providers and callbacks:
typescript
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      if (token) {
        session.user.id = token.id as string;
      }
      return session;
    },
  },
  pages: {
    signIn: "/login",
    error: "/error",
  },
});
在项目根目录创建
auth.ts
文件,配置提供商与回调函数:
typescript
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      if (token) {
        session.user.id = token.id as string;
      }
      return session;
    },
  },
  pages: {
    signIn: "/login",
    error: "/error",
  },
});

4. Create API Route Handler

4. 创建API路由处理器

Create
app/api/auth/[...nextauth]/route.ts
:
typescript
export { GET, POST } from "@/auth";
创建
app/api/auth/[...nextauth]/route.ts
文件:
typescript
export { GET, POST } from "@/auth";

5. Add Middleware for Route Protection

5. 添加路由保护中间件

Create
middleware.ts
in the project root:
typescript
import { auth } from "@/auth";
import { NextResponse } from "next/server";

export default auth((req) => {
  const { nextUrl } = req;
  const isLoggedIn = !!req.auth;
  const isApiAuthRoute = nextUrl.pathname.startsWith("/api/auth");
  const isPublicRoute = ["/", "/login", "/register"].includes(nextUrl.pathname);
  const isProtectedRoute = nextUrl.pathname.startsWith("/dashboard");

  if (isApiAuthRoute) return NextResponse.next();

  if (!isLoggedIn && isProtectedRoute) {
    return NextResponse.redirect(new URL("/login", nextUrl));
  }

  if (isLoggedIn && nextUrl.pathname === "/login") {
    return NextResponse.redirect(new URL("/dashboard", nextUrl));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)"],
};
在项目根目录创建
middleware.ts
文件:
typescript
import { auth } from "@/auth";
import { NextResponse } from "next/server";

export default auth((req) => {
  const { nextUrl } = req;
  const isLoggedIn = !!req.auth;
  const isApiAuthRoute = nextUrl.pathname.startsWith("/api/auth");
  const isPublicRoute = ["/", "/login", "/register"].includes(nextUrl.pathname);
  const isProtectedRoute = nextUrl.pathname.startsWith("/dashboard");

  if (isApiAuthRoute) return NextResponse.next();

  if (!isLoggedIn && isProtectedRoute) {
    return NextResponse.redirect(new URL("/login", nextUrl));
  }

  if (isLoggedIn && nextUrl.pathname === "/login") {
    return NextResponse.redirect(new URL("/dashboard", nextUrl));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)"],
};

6. Access Session in Server Components

6. 在Server Components中访问会话

Use the
auth()
function to access session in Server Components:
tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();

  if (!session) {
    redirect("/login");
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
    </div>
  );
}
使用
auth()
函数在Server Components中获取会话信息:
tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();

  if (!session) {
    redirect("/login");
  }

  return (
    <div>
      <h1>欢迎回来,{session.user.name}</h1>
    </div>
  );
}

7. Secure Server Actions

7. 保护Server Actions

Always verify authentication in Server Actions before mutations:
tsx
"use server";

import { auth } from "@/auth";

export async function createTodo(formData: FormData) {
  const session = await auth();

  if (!session?.user) {
    throw new Error("Unauthorized");
  }

  // Proceed with protected action
  const title = formData.get("title") as string;
  await db.todo.create({
    data: { title, userId: session.user.id },
  });
}
在执行数据变更前,务必在Server Actions中验证用户认证状态:
tsx
"use server";

import { auth } from "@/auth";

export async function createTodo(formData: FormData) {
  const session = await auth();

  if (!session?.user) {
    throw new Error("未授权访问");
  }

  // 执行受保护的操作
  const title = formData.get("title") as string;
  await db.todo.create({
    data: { title, userId: session.user.id },
  });
}

8. Handle Sign-In/Sign-Out

8. 处理登录/登出逻辑

Create a login page with server action:
tsx
// app/login/page.tsx
import { signIn } from "@/auth";
import { redirect } from "next/navigation";

export default function LoginPage() {
  async function handleLogin(formData: FormData) {
    "use server";

    const result = await signIn("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: false,
    });

    if (result?.error) {
      return { error: "Invalid credentials" };
    }

    redirect("/dashboard");
  }

  return (
    <form action={handleLogin}>
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Password" required />
      <button type="submit">Sign In</button>
    </form>
  );
}
For client-side sign-out:
tsx
"use client";

import { signOut } from "next-auth/react";

export function SignOutButton() {
  return <button onClick={() => signOut()}>Sign Out</button>;
}
创建包含Server Action的登录页面:
tsx
// app/login/page.tsx
import { signIn } from "@/auth";
import { redirect } from "next/navigation";

export default function LoginPage() {
  async function handleLogin(formData: FormData) {
    "use server";

    const result = await signIn("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: false,
    });

    if (result?.error) {
      return { error: "凭证无效" };
    }

    redirect("/dashboard");
  }

  return (
    <form action={handleLogin}>
      <input name="email" type="email" placeholder="邮箱" required />
      <input name="password" type="password" placeholder="密码" required />
      <button type="submit">登录</button>
    </form>
  );
}
客户端登出实现:
tsx
"use client";

import { signOut } from "next-auth/react";

export function SignOutButton() {
  return <button onClick={() => signOut()}>登出</button>;
}

9. Implement Role-Based Access

9. 实现基于角色的访问控制

Check roles in Server Components:
tsx
import { auth } from "@/auth";
import { unauthorized } from "next/navigation";

export default async function AdminPage() {
  const session = await auth();

  if (session?.user?.role !== "admin") {
    unauthorized();
  }

  return <AdminDashboard />;
}
在Server Components中校验用户角色:
tsx
import { auth } from "@/auth";
import { unauthorized } from "next/navigation";

export default async function AdminPage() {
  const session = await auth();

  if (session?.user?.role !== "admin") {
    unauthorized();
  }

  return <AdminDashboard />;
}

10. Extend TypeScript Types

10. 扩展TypeScript类型

Create
types/next-auth.d.ts
for type-safe sessions:
typescript
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: "user" | "admin";
    } & DefaultSession["user"];
  }

  interface User {
    role?: "user" | "admin";
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id?: string;
    role?: "user" | "admin";
  }
}
创建
types/next-auth.d.ts
文件,实现类型安全的会话:
typescript
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: "user" | "admin";
    } & DefaultSession["user"];
  }

  interface User {
    role?: "user" | "admin";
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id?: string;
    role?: "user" | "admin";
  }
}

Examples

示例

Example 1: Complete Protected Dashboard

示例1:完整的受保护仪表盘

Input: User needs a dashboard accessible only to authenticated users
Implementation:
tsx
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { getUserTodos } from "@/app/lib/data";

export default async function DashboardPage() {
  const session = await auth();

  if (!session?.user?.id) {
    redirect("/login");
  }

  const todos = await getUserTodos(session.user.id);

  return (
    <main>
      <h1>Welcome, {session.user.name}</h1>
      <p>Email: {session.user.email}</p>
      <TodoList todos={todos} />
    </main>
  );
}
Output: Dashboard renders only for authenticated users, with their specific data.
需求: 仅允许已认证用户访问仪表盘
实现代码:
tsx
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { getUserTodos } from "@/app/lib/data";

export default async function DashboardPage() {
  const session = await auth();

  if (!session?.user?.id) {
    redirect("/login");
  }

  const todos = await getUserTodos(session.user.id);

  return (
    <main>
      <h1>欢迎回来,{session.user.name}</h1>
      <p>邮箱:{session.user.email}</p>
      <TodoList todos={todos} />
    </main>
  );
}
效果: 仅已认证用户可查看仪表盘,并展示其专属数据。

Example 2: Role-Based Admin Panel

示例2:基于角色的管理面板

Input: Admin panel should be accessible only to users with "admin" role
Implementation:
tsx
// app/admin/page.tsx
import { auth } from "@/auth";
import { unauthorized } from "next/navigation";

export default async function AdminPage() {
  const session = await auth();

  if (session?.user?.role !== "admin") {
    unauthorized();
  }

  return (
    <main>
      <h1>Admin Panel</h1>
      <p>Welcome, administrator {session.user.name}</p>
    </main>
  );
}
Output: Only admin users see the panel; others get 401 error.
需求: 管理面板仅允许拥有"admin"角色的用户访问
实现代码:
tsx
// app/admin/page.tsx
import { auth } from "@/auth";
import { unauthorized } from "next/navigation";

export default async function AdminPage() {
  const session = await auth();

  if (session?.user?.role !== "admin") {
    unauthorized();
  }

  return (
    <main>
      <h1>管理面板</h1>
      <p>欢迎您,管理员 {session.user.name}</p>
    </main>
  );
}
效果: 仅管理员用户可查看面板,其他用户将收到401未授权错误。

Example 3: Secure Server Action with Form

示例3:受保护的Server Action表单

Input: Form submission should only work for authenticated users
Implementation:
tsx
// app/components/create-todo-form.tsx
"use server";

import { auth } from "@/auth";
import { revalidatePath } from "next/cache";

export async function createTodo(formData: FormData) {
  const session = await auth();

  if (!session?.user?.id) {
    throw new Error("Unauthorized");
  }

  const title = formData.get("title") as string;

  await db.todo.create({
    data: {
      title,
      userId: session.user.id,
    },
  });

  revalidatePath("/dashboard");
}

// Usage in component
export function CreateTodoForm() {
  return (
    <form action={createTodo}>
      <input name="title" placeholder="New todo..." required />
      <button type="submit">Add Todo</button>
    </form>
  );
}
Output: Todo created only for authenticated user; unauthorized requests throw error.
需求: 仅已认证用户可提交表单
实现代码:
tsx
// app/components/create-todo-form.tsx
"use server";

import { auth } from "@/auth";
import { revalidatePath } from "next/cache";

export async function createTodo(formData: FormData) {
  const session = await auth();

  if (!session?.user?.id) {
    throw new Error("未授权访问");
  }

  const title = formData.get("title") as string;

  await db.todo.create({
    data: {
      title,
      userId: session.user.id,
    },
  });

  revalidatePath("/dashboard");
}

// 组件使用示例
export function CreateTodoForm() {
  return (
    <form action={createTodo}>
      <input name="title" placeholder="新建待办..." required />
      <button type="submit">添加待办</button>
    </form>
  );
}
效果: 仅已认证用户可创建待办事项,未授权请求将抛出错误。

Example 4: OAuth Sign-In Button

示例4:OAuth登录按钮

Input: User should be able to sign in with GitHub
Implementation:
tsx
// components/auth/sign-in-button.tsx
"use client";

import { signIn, signOut, useSession } from "next-auth/react";

export function AuthButton() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <button disabled>Loading...</button>;
  }

  if (session) {
    return (
      <button onClick={() => signOut()}>
        Sign out {session.user?.name}
      </button>
    );
  }

  return (
    <button onClick={() => signIn("github")}>
      Sign in with GitHub
    </button>
  );
}
Output: Button shows "Sign in with GitHub" for unauthenticated users, "Sign out {name}" for authenticated users.
需求: 用户可通过GitHub账号登录
实现代码:
tsx
// components/auth/sign-in-button.tsx
"use client";

import { signIn, signOut, useSession } from "next-auth/react";

export function AuthButton() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <button disabled>加载中...</button>;
  }

  if (session) {
    return (
      <button onClick={() => signOut()}>
        登出 {session.user?.name}
      </button>
    );
  }

  return (
    <button onClick={() => signIn("github")}>
      使用GitHub登录
    </button>
  );
}
效果: 未认证用户看到"使用GitHub登录"按钮,已认证用户看到"登出 {用户名}"按钮。

Example 5: Credentials Provider Login

示例5:凭证式登录提供商

Input: Implement email/password login
Implementation:
tsx
// auth.ts
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }

        const user = await db.user.findUnique({
          where: { email: credentials.email },
        });

        if (!user || !user.password) {
          return null;
        }

        const isValid = await bcrypt.compare(
          credentials.password,
          user.password
        );

        return isValid
          ? { id: user.id, email: user.email, name: user.name }
          : null;
      },
    }),
  ],
});
Output: Users can authenticate with email/password against your database.
需求: 实现邮箱/密码登录
实现代码:
tsx
// auth.ts
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }

        const user = await db.user.findUnique({
          where: { email: credentials.email },
        });

        if (!user || !user.password) {
          return null;
        }

        const isValid = await bcrypt.compare(
          credentials.password,
          user.password
        );

        return isValid
          ? { id: user.id, email: user.email, name: user.name }
          : null;
      },
    }),
  ],
});
效果: 用户可通过邮箱/密码与数据库中的凭证进行认证。

Best Practices

最佳实践

  1. Use Server Components by default - Access session directly without client-side JavaScript
  2. Minimize Client Components - Only use
    useSession()
    for reactive session updates
  3. Cache session checks - Use React's
    cache()
    for repeated lookups in the same render
  4. Middleware for optimistic checks - Redirect quickly, but always re-verify in Server Actions
  5. Treat Server Actions like API endpoints - Always authenticate before mutations
  6. Never hardcode secrets - Use environment variables for all credentials
  7. Implement proper error handling - Return appropriate HTTP status codes
  8. Use TypeScript type extensions - Extend NextAuth types for custom fields
  9. Separate auth logic - Create a DAL (Data Access Layer) for consistent checks
  10. Test authentication flows - Mock
    auth()
    function in unit tests
  1. 优先使用Server Components - 无需客户端JavaScript即可直接访问会话
  2. 减少Client Components使用 - 仅在需要响应式会话更新时使用
    useSession()
  3. 缓存会话校验结果 - 在同一次渲染中,使用React的
    cache()
    缓存重复查询
  4. 用Middleware做乐观校验 - 快速重定向,但务必在Server Actions中再次验证
  5. 将Server Actions视为API端点 - 执行数据变更前必须验证用户身份
  6. 切勿硬编码密钥 - 所有凭证均使用环境变量存储
  7. 实现完善的错误处理 - 返回合适的HTTP状态码
  8. 扩展TypeScript类型 - 扩展NextAuth类型以支持自定义字段
  9. 分离认证逻辑 - 创建数据访问层(DAL)以确保校验逻辑一致性
  10. 测试认证流程 - 在单元测试中模拟
    auth()
    函数

Constraints and Warnings

约束与注意事项

Critical Limitations

关键限制

  • Middleware runs on Edge runtime - Cannot use Node.js APIs like database drivers
  • Server Components cannot set cookies - Use Server Actions for cookie operations
  • Session callback timing - Only called on session creation/access, not every request
  • Middleware运行在Edge runtime - 无法使用Node.js API(如数据库驱动)
  • Server Components无法设置Cookie - 需使用Server Actions处理Cookie操作
  • 会话回调触发时机 - 仅在会话创建/访问时触发,并非每次请求都执行

Common Mistakes

常见错误

tsx
// ❌ WRONG: Setting cookies in Server Component
export default async function Page() {
  cookies().set("key", "value"); // Won't work
}

// ✅ CORRECT: Use Server Action
async function setCookieAction() {
  "use server";
  cookies().set("key", "value");
}
typescript
// ❌ WRONG: Database queries in Middleware
export default auth(async (req) => {
  const user = await db.user.findUnique(); // Won't work in Edge
});

// ✅ CORRECT: Use only Edge-compatible APIs
export default auth(async (req) => {
  const session = req.auth; // This works
});
tsx
// ❌ 错误:在Server Components中设置Cookie
export default async function Page() {
  cookies().set("key", "value"); // 此操作无效
}

// ✅ 正确:使用Server Action
async function setCookieAction() {
  "use server";
  cookies().set("key", "value");
}
typescript
// ❌ 错误:在Middleware中执行数据库查询
export default auth(async (req) => {
  const user = await db.user.findUnique(); // Edge环境下无法运行
});

// ✅ 正确:仅使用Edge兼容的API
export default auth(async (req) => {
  const session = req.auth; // 此操作有效
});

Security Considerations

安全考量

  • Always verify authentication in Server Actions - middleware alone is not enough
  • Use
    unauthorized()
    for unauthenticated access,
    redirect()
    for other cases
  • Store sensitive tokens in
    httpOnly
    cookies
  • Validate all user input before processing
  • Use HTTPS in production
  • Set appropriate cookie
    sameSite
    attributes
  • 务必在Server Actions中验证用户身份 - 仅靠Middleware保护并不足够
  • 对未授权访问使用
    unauthorized()
    ,其他场景使用
    redirect()
  • 敏感令牌存储在
    httpOnly
    Cookie中
  • 处理前验证所有用户输入
  • 生产环境必须使用HTTPS
  • 设置合适的Cookie
    sameSite
    属性

References

参考资料

  • references/authjs-setup.md - Complete Auth.js 5 setup guide with Prisma/Drizzle adapters
  • references/oauth-providers.md - Provider-specific configurations (GitHub, Google, Discord, Auth0, etc.)
  • references/database-adapter.md - Database session management with Prisma, Drizzle, and custom adapters
  • references/testing-patterns.md - Testing authentication flows with Vitest and Playwright
  • references/authjs-setup.md - 包含Prisma/Drizzle适配器的完整Auth.js 5搭建指南
  • references/oauth-providers.md - 各提供商的专属配置(GitHub、Google、Discord、Auth0等)
  • references/database-adapter.md - 使用Prisma、Drizzle及自定义适配器的数据库会话管理
  • references/testing-patterns.md - 使用Vitest和Playwright测试认证流程