authjs-skills

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Links

相关链接

Installation

安装步骤

sh
pnpm add next-auth@beta
Note: Auth.js v5 is currently in beta. Use
next-auth@beta
to install the latest v5 version.
sh
pnpm add next-auth@beta
注意:Auth.js v5目前处于测试版。请使用
next-auth@beta
安装最新的v5版本。

What's New in Auth.js v5?

Auth.js v5的新特性

Key Changes from v4

与v4版本的主要变化

  • Simplified Configuration: More streamlined setup with better TypeScript support
  • Universal
    auth()
    Export
    : Single function for authentication across all contexts
  • Enhanced Security: Improved CSRF protection and session handling
  • Edge Runtime Support: Full compatibility with Edge Runtime and middleware
  • Better Type Safety: Improved TypeScript definitions throughout
  • 简化配置:更流畅的设置流程,增强TypeScript支持
  • 通用
    auth()
    导出
    :单一函数支持所有上下文的认证操作
  • 增强安全性:改进CSRF保护和会话处理
  • Edge Runtime支持:完全兼容Edge Runtime和中间件
  • 更好的类型安全:全面优化TypeScript类型定义

Environment Variables

环境变量

Required Environment Variables

必填环境变量

env
undefined
env
undefined

Auth.js Configuration

Auth.js配置

AUTH_SECRET=your_secret_key_here
AUTH_SECRET=your_secret_key_here

Google OAuth (if using Google provider)

Google OAuth(如果使用Google提供器)

AUTH_GOOGLE_ID=your_google_client_id AUTH_GOOGLE_SECRET=your_google_client_secret
AUTH_GOOGLE_ID=your_google_client_id AUTH_GOOGLE_SECRET=your_google_client_secret

For production deployments

生产环境部署

For development (optional, defaults to http://localhost:3000)

开发环境(可选,默认值为http://localhost:3000)

undefined
undefined

Generating AUTH_SECRET

生成AUTH_SECRET

sh
undefined
sh
undefined

Generate a random secret (Unix/Linux/macOS)

生成随机密钥(Unix/Linux/macOS)

openssl rand -base64 32
openssl rand -base64 32

Alternative using Node.js

使用Node.js的替代方法

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Using pnpm

使用pnpm

pnpm dlx auth secret

**Important**: Never commit `AUTH_SECRET` to version control. Use `.env.local` for development.
pnpm dlx auth secret

**重要提示**:切勿将`AUTH_SECRET`提交到版本控制系统。开发环境请使用`.env.local`文件存储。

Basic Setup (Next.js App Router)

基础设置(Next.js App Router)

1. Create
auth.ts
Configuration File

1. 创建
auth.ts
配置文件

Create
auth.ts
at the project root (next to
package.json
):
typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        // TODO: Implement your authentication logic here
        // This is a basic example - see Credentials Provider section below for complete implementation
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        // Example: validate against database (placeholder)
        // See "Credentials Provider" section for full implementation with bcrypt
        const user = { id: "1", email: credentials.email, name: "User" } // Replace with actual DB lookup
        
        if (!user) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
        }
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin',
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // Return true if user is authenticated
      return !!auth
    },
  },
})
Note: This is a basic setup example. For production-ready credentials authentication, see the "Credentials Provider" section below which includes proper password hashing with bcrypt and database integration.
在项目根目录(
package.json
同级)创建
auth.ts
typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        // TODO:在此实现你的认证逻辑
        // 这是基础示例 - 如需完整实现,请参考下方的「凭证提供器」章节,包含bcrypt密码哈希和数据库集成
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        // 示例:与数据库验证(占位符)
        // 请查看「凭证提供器」章节获取包含bcrypt的完整实现
        const user = { id: "1", email: credentials.email, name: "User" } // 替换为实际的数据库查询逻辑
        
        if (!user) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
        }
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin',
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // 用户已认证则返回true
      return !!auth
    },
  },
})
注意:这是基础设置示例。如需生产环境可用的凭证认证,请参考下方的「凭证提供器」章节,其中包含使用bcrypt的正确密码哈希和数据库集成。

2. Create API Route Handler

2. 创建API路由处理器

Create
app/api/auth/[...nextauth]/route.ts
:
typescript
import { handlers } from "@/auth"

export const { GET, POST } = handlers
创建
app/api/auth/[...nextauth]/route.ts
typescript
import { handlers } from "@/auth"

export const { GET, POST } = handlers

3. Add Middleware (Optional but Recommended)

3. 添加中间件(可选但推荐)

Create
middleware.ts
at the project root:
typescript
export { auth as middleware } from "@/auth"

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
For more control:
typescript
import { auth } from "@/auth"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
  
  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL('/auth/signin', req.url))
  }
})

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
}
在项目根目录创建
middleware.ts
typescript
export { auth as middleware } from "@/auth"

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
如需更精细的控制:
typescript
import { auth } from "@/auth"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
  
  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL('/auth/signin', req.url))
  }
})

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
}

Google OAuth Provider

Google OAuth提供器

1. Google Cloud Console Setup

1. Google Cloud控制台设置

  1. Go to Google Cloud Console
  2. Create a new project or select existing
  3. Enable Google+ API
  4. Create OAuth 2.0 credentials:
    • Application type: Web application
    • Authorized redirect URIs:
      • Development:
        http://localhost:3000/api/auth/callback/google
      • Production:
        https://yourdomain.com/api/auth/callback/google
  5. Copy Client ID and Client Secret to
    .env.local
  1. 访问Google Cloud控制台
  2. 创建新项目或选择现有项目
  3. 启用Google+ API
  4. 创建OAuth 2.0凭证:
    • 应用类型:Web应用
    • 授权重定向URI:
      • 开发环境:
        http://localhost:3000/api/auth/callback/google
      • 生产环境:
        https://yourdomain.com/api/auth/callback/google
  5. 将客户端ID和客户端密钥复制到
    .env.local
    文件中

2. Configuration

2. 配置代码

typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code"
        }
      }
    }),
  ],
})
typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code"
        }
      }
    }),
  ],
})

3. Google Provider Options

3. Google提供器可选配置

typescript
Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  // Request additional scopes
  authorization: {
    params: {
      scope: "openid email profile",
      prompt: "select_account", // Force account selection
    }
  },
  // Allow specific domains only
  allowDangerousEmailAccountLinking: false,
})
typescript
Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  // 请求额外的权限范围
  authorization: {
    params: {
      scope: "openid email profile",
      prompt: "select_account", // 强制用户选择账号
    }
  },
  // 仅允许特定域名的账号
  allowDangerousEmailAccountLinking: false,
})

Credentials Provider (Username/Password)

凭证提供器(用户名/密码登录)

Required Dependencies

所需依赖

sh
undefined
sh
undefined

Install required packages for credentials provider

安装凭证提供器所需的包

pnpm add bcryptjs zod pnpm add -D @types/bcryptjs
undefined
pnpm add bcryptjs zod pnpm add -D @types/bcryptjs
undefined

1. Basic Configuration

1. 基础配置

typescript
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { z } from "zod"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email", placeholder: "user@example.com" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        try {
          const { email, password } = credentialsSchema.parse(credentials)
          
          // Fetch user from database
          const user = await prisma.user.findUnique({
            where: { email },
          })

          if (!user) {
            throw new Error("User not found")
          }

          // Verify password
          const isValidPassword = await bcrypt.compare(password, user.hashedPassword)
          
          if (!isValidPassword) {
            throw new Error("Invalid password")
          }

          // Return user object (must include id)
          return {
            id: user.id,
            email: user.email,
            name: user.name,
            image: user.image,
          }
        } catch (error) {
          console.error("Authentication error:", error)
          return null
        }
      },
    }),
  ],
  session: {
    strategy: "jwt", // Required for credentials provider
  },
})
typescript
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { z } from "zod"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email", placeholder: "user@example.com" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        try {
          const { email, password } = credentialsSchema.parse(credentials)
          
          // 从数据库获取用户
          const user = await prisma.user.findUnique({
            where: { email },
          })

          if (!user) {
            throw new Error("用户不存在")
          }

          // 验证密码
          const isValidPassword = await bcrypt.compare(password, user.hashedPassword)
          
          if (!isValidPassword) {
            throw new Error("密码无效")
          }

          // 返回用户对象(必须包含id字段)
          return {
            id: user.id,
            email: user.email,
            name: user.name,
            image: user.image,
          }
        } catch (error) {
          console.error("认证错误:", error)
          return null
        }
      },
    }),
  ],
  session: {
    strategy: "jwt", // 凭证提供器必须使用此策略
  },
})

2. User Registration Example

2. 用户注册示例

typescript
// app/api/auth/register/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { z } from "zod"
import { prisma } from "@/lib/prisma"

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2),
})

export async function POST(req: Request) {
  try {
    const body = await req.json()
    const { email, password, name } = registerSchema.parse(body)

    // Check if user exists
    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "User already exists" },
        { status: 400 }
      )
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10)

    // Create user
    const user = await prisma.user.create({
      data: {
        email,
        name,
        hashedPassword,
      },
    })

    return NextResponse.json(
      { message: "User created successfully", userId: user.id },
      { status: 201 }
    )
  } catch (error) {
    console.error("Registration error:", error)
    return NextResponse.json(
      { error: "Failed to register user" },
      { status: 500 }
    )
  }
}
typescript
// app/api/auth/register/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { z } from "zod"
import { prisma } from "@/lib/prisma"

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2),
})

export async function POST(req: Request) {
  try {
    const body = await req.json()
    const { email, password, name } = registerSchema.parse(body)

    // 检查用户是否已存在
    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "用户已存在" },
        { status: 400 }
      )
    }

    // 哈希密码
    const hashedPassword = await bcrypt.hash(password, 10)

    // 创建用户
    const user = await prisma.user.create({
      data: {
        email,
        name,
        hashedPassword,
      },
    })

    return NextResponse.json(
      { message: "用户创建成功", userId: user.id },
      { status: 201 }
    )
  } catch (error) {
    console.error("注册错误:", error)
    return NextResponse.json(
      { error: "用户注册失败" },
      { status: 500 }
    )
  }
}

Using Auth in Components

在组件中使用Auth

Server Components

服务端组件

typescript
import { auth } from "@/auth"

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

  if (!session?.user) {
    return <div>Not authenticated</div>
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
    </div>
  )
}
typescript
import { auth } from "@/auth"

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

  if (!session?.user) {
    return <div>未认证</div>
  }

  return (
    <div>
      <h1>欢迎回来,{session.user.name}</h1>
      <p>邮箱:{session.user.email}</p>
    </div>
  )
}

Server Actions

服务端操作

typescript
"use server"

import { auth } from "@/auth"
import { revalidatePath } from "next/cache"
import { prisma } from "@/lib/prisma"

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

  if (!session?.user) {
    throw new Error("Not authenticated")
  }

  const name = formData.get("name") as string

  // Update database
  await prisma.user.update({
    where: { id: session.user.id },
    data: { name },
  })

  revalidatePath("/profile")
}
typescript
"use server"

import { auth } from "@/auth"
import { revalidatePath } from "next/cache"
import { prisma } from "@/lib/prisma"

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

  if (!session?.user) {
    throw new Error("未认证")
  }

  const name = formData.get("name") as string

  // 更新数据库
  await prisma.user.update({
    where: { id: session.user.id },
    data: { name },
  })

  revalidatePath("/profile")
}

Client Components (with SessionProvider)

客户端组件(使用SessionProvider)

typescript
// app/providers.tsx
"use client"

import { SessionProvider } from "next-auth/react"

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}
typescript
// app/layout.tsx
import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
typescript
// app/components/user-profile.tsx
"use client"

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

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

  if (status === "loading") {
    return <div>Loading...</div>
  }

  if (!session) {
    return (
      <button onClick={() => signIn()}>
        Sign In
      </button>
    )
  }

  return (
    <div>
      <p>Signed in as {session.user?.email}</p>
      <button onClick={() => signOut()}>
        Sign Out
      </button>
    </div>
  )
}
typescript
// app/providers.tsx
"use client"

import { SessionProvider } from "next-auth/react"

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}
typescript
// app/layout.tsx
import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
typescript
// app/components/user-profile.tsx
"use client"

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

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

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

  if (!session) {
    return (
      <button onClick={() => signIn()}>
        登录
      </button>
    )
  }

  return (
    <div>
      <p>当前登录账号:{session.user?.email}</p>
      <button onClick={() => signOut()}>
        退出登录
      </button>
    </div>
  )
}

Sign In/Out Actions

登录/退出操作

Programmatic Sign In

程序化登录

typescript
import { signIn } from "@/auth"

// Server Action
export async function handleSignIn(provider: string) {
  "use server"
  await signIn(provider)
}

// With credentials
export async function handleCredentialsSignIn(formData: FormData) {
  "use server"
  await signIn("credentials", formData)
}

// With redirect
export async function handleGoogleSignIn() {
  "use server"
  await signIn("google", { redirectTo: "/dashboard" })
}
typescript
import { signIn } from "@/auth"

// 服务端操作
export async function handleSignIn(provider: string) {
  "use server"
  await signIn(provider)
}

// 使用凭证登录
export async function handleCredentialsSignIn(formData: FormData) {
  "use server"
  await signIn("credentials", formData)
}

// 登录后重定向
export async function handleGoogleSignIn() {
  "use server"
  await signIn("google", { redirectTo: "/dashboard" })
}

Sign In Form Component

登录表单组件

typescript
// app/auth/signin/page.tsx
import { signIn } from "@/auth"

export default function SignInPage() {
  return (
    <div>
      <h1>Sign In</h1>
      
      {/* Google OAuth */}
      <form
        action={async () => {
          "use server"
          await signIn("google")
        }}
      >
        <button type="submit">Sign in with Google</button>
      </form>

      {/* Credentials */}
      <form
        action={async (formData) => {
          "use server"
          await signIn("credentials", formData)
        }}
      >
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Sign In</button>
      </form>
    </div>
  )
}
typescript
// app/auth/signin/page.tsx
import { signIn } from "@/auth"

export default function SignInPage() {
  return (
    <div>
      <h1>登录</h1>
      
      {/* Google OAuth登录 */}
      <form
        action={async () => {
          "use server"
          await signIn("google")
        }}
      >
        <button type="submit">使用Google登录</button>
      </form>

      {/* 凭证登录 */}
      <form
        action={async (formData) => {
          "use server"
          await signIn("credentials", formData)
        }}
      >
        <input name="email" type="email" placeholder="邮箱" required />
        <input name="password" type="password" placeholder="密码" required />
        <button type="submit">登录</button>
      </form>
    </div>
  )
}

Sign Out

退出登录

typescript
import { signOut } from "@/auth"

export default function SignOutButton() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}
typescript
import { signOut } from "@/auth"

export default function SignOutButton() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">退出登录</button>
    </form>
  )
}

Session Management

会话管理

Session Strategy

会话策略

Auth.js v5 supports two session strategies:
  1. JWT (Default): Stores session in encrypted JWT token
  2. Database: Stores session in database
typescript
export const { handlers, signIn, signOut, auth } = NextAuth({
  session: {
    strategy: "jwt", // or "database"
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
  },
})
Auth.js v5支持两种会话策略:
  1. JWT(默认):将会话存储在加密的JWT令牌中
  2. 数据库:将会话存储在数据库中
typescript
export const { handlers, signIn, signOut, auth } = NextAuth({
  session: {
    strategy: "jwt", // 或 "database"
    maxAge: 30 * 24 * 60 * 60, // 30天
    updateAge: 24 * 60 * 60, // 24小时
  },
})

Extending the Session

扩展会话字段

typescript
import NextAuth from "next-auth"
import type { DefaultSession } from "next-auth"

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

export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      return token
    },
    session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
})
typescript
import NextAuth from "next-auth"
import type { DefaultSession } from "next-auth"

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

export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      return token
    },
    session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
})

Callbacks

回调函数

Essential Callbacks

核心回调函数

typescript
export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    // Called when user signs in
    async signIn({ user, account, profile }) {
      // Return true to allow sign in, false to deny
      // Example: Check if email is verified
      if (account?.provider === "google") {
        return profile?.email_verified === true
      }
      return true
    },

    // Called whenever a JWT is created or updated
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id
      }
      if (account) {
        token.accessToken = account.access_token
      }
      return token
    },

    // Called whenever a session is checked
    async session({ session, token }) {
      session.user.id = token.id as string
      session.accessToken = token.accessToken as string
      return session
    },

    // Called on middleware and server-side auth checks
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = request.nextUrl.pathname.startsWith("/dashboard")
      
      if (isOnDashboard) {
        return isLoggedIn
      }
      
      return true
    },

    // Called when user is redirected
    async redirect({ url, baseUrl }) {
      // Allows relative callback URLs
      if (url.startsWith("/")) return `${baseUrl}${url}`
      // Allows callback URLs on the same origin
      else if (new URL(url).origin === baseUrl) return url
      return baseUrl
    },
  },
})
typescript
export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    // 用户登录时调用
    async signIn({ user, account, profile }) {
      // 返回true允许登录,返回false拒绝登录
      // 示例:检查邮箱是否已验证
      if (account?.provider === "google") {
        return profile?.email_verified === true
      }
      return true
    },

    // JWT创建或更新时调用
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id
      }
      if (account) {
        token.accessToken = account.access_token
      }
      return token
    },

    // 会话校验时调用
    async session({ session, token }) {
      session.user.id = token.id as string
      session.accessToken = token.accessToken as string
      return session
    },

    // 中间件和服务端认证校验时调用
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = request.nextUrl.pathname.startsWith("/dashboard")
      
      if (isOnDashboard) {
        return isLoggedIn
      }
      
      return true
    },

    // 用户重定向时调用
    async redirect({ url, baseUrl }) {
      // 允许相对路径的回调URL
      if (url.startsWith("/")) return `${baseUrl}${url}`
      // 允许同域名的回调URL
      else if (new URL(url).origin === baseUrl) return url
      return baseUrl
    },
  },
})

Database Adapter (Optional)

数据库适配器(可选)

For persisting users, accounts, and sessions in a database, install the Prisma adapter:
sh
pnpm add @auth/prisma-adapter
Then configure it in your
auth.ts
:
typescript
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: "database",
  },
  providers: [
    // ... providers
  ],
})
Required Prisma schema:
prisma
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
如需将用户、账号和会话持久化到数据库,请安装Prisma适配器:
sh
pnpm add @auth/prisma-adapter
然后在
auth.ts
中配置:
typescript
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: "database",
  },
  providers: [
    // ... 你的提供器配置
  ],
})
所需的Prisma schema:
prisma
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

API Routes

API路由

Custom API Endpoints

自定义API端点

typescript
// app/api/user/route.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export async function GET() {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }

  return NextResponse.json({
    user: session.user,
  })
}
typescript
// app/api/user/route.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export async function GET() {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "未授权" },
      { status: 401 }
    )
  }

  return NextResponse.json({
    user: session.user,
  })
}

Protected Route Helper

受保护路由工具函数

typescript
// lib/auth-helpers.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
import type { Session } from "next-auth"

export async function withAuth(
  handler: (session: Session) => Promise<NextResponse>
) {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }

  return handler(session)
}

// Usage
export async function GET() {
  return withAuth(async (session) => {
    return NextResponse.json({ userId: session.user.id })
  })
}
typescript
// lib/auth-helpers.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
import type { Session } from "next-auth"

export async function withAuth(
  handler: (session: Session) => Promise<NextResponse>
) {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "未授权" },
      { status: 401 }
    )
  }

  return handler(session)
}

// 使用示例
export async function GET() {
  return withAuth(async (session) => {
    return NextResponse.json({ userId: session.user.id })
  })
}

Best Practices

最佳实践

Security

安全方面

  • Always hash passwords: Use bcrypt, argon2, or similar
  • Use HTTPS in production: Required for secure cookie transmission
  • Validate environment variables: Check AUTH_SECRET and provider credentials
  • Set secure cookie options:
    typescript
    cookies: {
      sessionToken: {
        name: `__Secure-next-auth.session-token`,
        options: {
          httpOnly: true,
          sameSite: 'lax',
          path: '/',
          secure: process.env.NODE_ENV === 'production',
        },
      },
    }
  • Implement rate limiting: Protect sign-in endpoints
  • Use CSRF protection: Enabled by default in v5
  • Validate redirects: Use the
    redirect
    callback to prevent open redirects
  • 始终哈希密码:使用bcrypt、argon2或类似的哈希算法
  • 生产环境使用HTTPS:安全的Cookie传输需要HTTPS
  • 验证环境变量:检查AUTH_SECRET和提供器凭证是否配置正确
  • 设置安全的Cookie选项:
    typescript
    cookies: {
      sessionToken: {
        name: `__Secure-next-auth.session-token`,
        options: {
          httpOnly: true,
          sameSite: 'lax',
          path: '/',
          secure: process.env.NODE_ENV === 'production',
        },
      },
    }
  • 实现速率限制:保护登录端点免受暴力攻击
  • 启用CSRF保护:v5版本默认启用
  • 验证重定向地址:使用
    redirect
    回调函数防止开放重定向漏洞

Session Management

会话管理

  • Use appropriate maxAge: Default 30 days, adjust based on security requirements
  • Update sessions regularly: Use
    updateAge
    to refresh session data
  • Handle session expiry gracefully: Provide clear UI feedback
  • Secure session storage: Use database strategy for sensitive applications
  • 设置合适的maxAge:默认30天,可根据安全需求调整
  • 定期更新会话:使用
    updateAge
    参数刷新会话数据
  • 优雅处理会话过期:提供清晰的UI提示
  • 安全存储会话:敏感应用请使用数据库策略

Provider Configuration

提供器配置

  • Google OAuth: Request minimum required scopes
  • Credentials: Always validate input with zod or similar
  • Multiple providers: Allow account linking carefully
  • Provider-specific logic: Use callbacks to handle provider differences
  • Google OAuth:仅请求必要的权限范围
  • 凭证登录:始终使用zod或类似工具验证输入
  • 多提供器:谨慎允许账号关联
  • 提供器特定逻辑:使用回调函数处理不同提供器的差异

Performance

性能方面

  • Cache session checks: Use middleware for route protection
  • Minimize database calls: Use JWT strategy when appropriate
  • Optimize database queries: Add indexes on frequently queried fields
  • Use Edge Runtime: For faster authentication checks in middleware
  • 缓存会话校验:使用中间件进行路由保护
  • 减少数据库调用:合适时使用JWT策略
  • 优化数据库查询:为频繁查询的字段添加索引
  • 使用Edge Runtime:提升中间件中认证校验的速度

Type Safety

类型安全

  • Extend types properly: Use module augmentation for custom session fields
  • Validate inputs: Use zod for runtime type checking
  • TypeScript strict mode: Enable for better type safety
  • 正确扩展类型:使用模块扩展自定义会话字段
  • 验证输入数据:使用zod进行运行时类型检查
  • 启用TypeScript严格模式:获得更好的类型安全保障

Common Patterns

常见模式

Protected Pages with Middleware

使用中间件保护页面

typescript
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const { pathname } = req.nextUrl

  // Public routes
  const publicRoutes = ['/auth/signin', '/auth/register', '/']
  if (publicRoutes.includes(pathname)) {
    return NextResponse.next()
  }

  // Protected routes
  if (!isLoggedIn) {
    const signInUrl = new URL('/auth/signin', req.url)
    signInUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(signInUrl)
  }

  // Role-based access
  const adminRoutes = ['/admin']
  if (adminRoutes.some(route => pathname.startsWith(route))) {
    if (req.auth.user.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  return NextResponse.next()
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
typescript
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const { pathname } = req.nextUrl

  // 公开路由
  const publicRoutes = ['/auth/signin', '/auth/register', '/']
  if (publicRoutes.includes(pathname)) {
    return NextResponse.next()
  }

  // 受保护路由
  if (!isLoggedIn) {
    const signInUrl = new URL('/auth/signin', req.url)
    signInUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(signInUrl)
  }

  // 基于角色的访问控制
  const adminRoutes = ['/admin']
  if (adminRoutes.some(route => pathname.startsWith(route))) {
    if (req.auth.user.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  return NextResponse.next()
})

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

Multi-Provider Setup

多提供器设置

typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      // ... credentials config
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile }) {
      // Link accounts with same email
      if (account?.provider !== "credentials") {
        const existingUser = await prisma.user.findUnique({
          where: { email: user.email },
        })
        
        if (existingUser) {
          // Link account to existing user
          await prisma.account.create({
            data: {
              userId: existingUser.id,
              type: account.type,
              provider: account.provider,
              providerAccountId: account.providerAccountId,
              access_token: account.access_token,
              refresh_token: account.refresh_token,
            },
          })
        }
      }
      return true
    },
  },
})
typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      // ... 你的凭证提供器配置
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile }) {
      // 关联相同邮箱的账号
      if (account?.provider !== "credentials") {
        const existingUser = await prisma.user.findUnique({
          where: { email: user.email },
        })
        
        if (existingUser) {
          // 将当前账号关联到已有用户
          await prisma.account.create({
            data: {
              userId: existingUser.id,
              type: account.type,
              provider: account.provider,
              providerAccountId: account.providerAccountId,
              access_token: account.access_token,
              refresh_token: account.refresh_token,
            },
          })
        }
      }
      return true
    },
  },
})

Custom Sign In Page

自定义登录页面

typescript
// app/auth/signin/page.tsx
import { signIn } from "@/auth"
import { redirect } from "next/navigation"

export default function SignInPage({
  searchParams,
}: {
  searchParams: { callbackUrl?: string }
}) {
  const callbackUrl = searchParams.callbackUrl || "/dashboard"

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-8 p-8">
        <h1 className="text-2xl font-bold text-center">Sign In</h1>
        
        {/* OAuth Providers */}
        <div className="space-y-4">
          <form
            action={async () => {
              "use server"
              await signIn("google", { redirectTo: callbackUrl })
            }}
          >
            <button 
              type="submit"
              className="w-full bg-white border border-gray-300 text-gray-700 py-2 px-4 rounded hover:bg-gray-50"
            >
              Continue with Google
            </button>
          </form>
        </div>

        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <div className="w-full border-t border-gray-300" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="bg-white px-2 text-gray-500">Or</span>
          </div>
        </div>

        {/* Credentials Form */}
        <form
          action={async (formData) => {
            "use server"
            try {
              await signIn("credentials", {
                email: formData.get("email"),
                password: formData.get("password"),
                redirectTo: callbackUrl,
              })
            } catch (error) {
              redirect(`/auth/signin?error=CredentialsSignin&callbackUrl=${callbackUrl}`)
            }
          }}
          className="space-y-4"
        >
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Password
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <button
            type="submit"
            className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
          >
            Sign In
          </button>
        </form>
      </div>
    </div>
  )
}
typescript
// app/auth/signin/page.tsx
import { signIn } from "@/auth"
import { redirect } from "next/navigation"

export default function SignInPage({
  searchParams,
}: {
  searchParams: { callbackUrl?: string }
}) {
  const callbackUrl = searchParams.callbackUrl || "/dashboard"

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-8 p-8">
        <h1 className="text-2xl font-bold text-center">登录</h1>
        
        {/* OAuth提供器登录 */}
        <div className="space-y-4">
          <form
            action={async () => {
              "use server"
              await signIn("google", { redirectTo: callbackUrl })
            }}
          >
            <button 
              type="submit"
              className="w-full bg-white border border-gray-300 text-gray-700 py-2 px-4 rounded hover:bg-gray-50"
            >
              继续使用Google登录
            </button>
          </form>
        </div>

        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <div className="w-full border-t border-gray-300" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="bg-white px-2 text-gray-500"></span>
          </div>
        </div>

        {/* 凭证登录表单 */}
        <form
          action={async (formData) => {
            "use server"
            try {
              await signIn("credentials", {
                email: formData.get("email"),
                password: formData.get("password"),
                redirectTo: callbackUrl,
              })
            } catch (error) {
              redirect(`/auth/signin?error=CredentialsSignin&callbackUrl=${callbackUrl}`)
            }
          }}
          className="space-y-4"
        >
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              邮箱
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              密码
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <button
            type="submit"
            className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
          >
            登录
          </button>
        </form>
      </div>
    </div>
  )
}

Role-Based Access Control (RBAC)

基于角色的访问控制(RBAC)

typescript
// lib/auth-rbac.ts
import { auth } from "@/auth"

export type Role = "admin" | "user" | "guest"

export async function checkRole(allowedRoles: Role[]) {
  const session = await auth()
  
  if (!session?.user) {
    return false
  }

  const userRole = session.user.role as Role
  return allowedRoles.includes(userRole)
}

// Usage in Server Component
export default async function AdminPage() {
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    redirect("/unauthorized")
  }

  return <div>Admin Dashboard</div>
}

// Usage in Server Action
export async function deleteUser(userId: string) {
  "use server"
  
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    throw new Error("Unauthorized")
  }

  const { prisma } = await import("@/lib/prisma")
  await prisma.user.delete({ where: { id: userId } })
}
typescript
// lib/auth-rbac.ts
import { auth } from "@/auth"

export type Role = "admin" | "user" | "guest"

export async function checkRole(allowedRoles: Role[]) {
  const session = await auth()
  
  if (!session?.user) {
    return false
  }

  const userRole = session.user.role as Role
  return allowedRoles.includes(userRole)
}

// 在服务端组件中使用
export default async function AdminPage() {
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    redirect("/unauthorized")
  }

  return <div>管理员控制台</div>
}

// 在服务端操作中使用
export async function deleteUser(userId: string) {
  "use server"
  
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    throw new Error("未授权")
  }

  const { prisma } = await import("@/lib/prisma")
  await prisma.user.delete({ where: { id: userId } })
}

Migration from v4 to v5

从v4迁移到v5

Key Differences

主要差异

  1. Import changes:
    next-auth
    package remains the same, but imports are simplified
  2. Universal
    auth()
    : Replace
    getServerSession
    with
    auth()
  3. Middleware: Use
    auth
    as middleware directly
  4. Configuration: More streamlined, fewer options needed
  1. 导入变化
    next-auth
    包名称不变,但导入方式更简洁
  2. 通用
    auth()
    :用
    auth()
    替代
    getServerSession
  3. 中间件:直接使用
    auth
    作为中间件
  4. 配置:更简化,所需选项更少

Migration Steps

迁移步骤示例

typescript
// v4 (old)
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function GET() {
  const session = await getServerSession(authOptions)
}

// v5 (new)
import { auth } from "@/auth"

export async function GET() {
  const session = await auth()
}
typescript
// v4 middleware (old)
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token }) => !!token,
  },
})

// v5 middleware (new)
export { auth as middleware } from "@/auth"
typescript
// v4(旧版)
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function GET() {
  const session = await getServerSession(authOptions)
}

// v5(新版)
import { auth } from "@/auth"

export async function GET() {
  const session = await auth()
}
typescript
// v4中间件(旧版)
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token }) => !!token,
  },
})

// v5中间件(新版)
export { auth as middleware } from "@/auth"

Troubleshooting

故障排除

Common Issues

常见问题

AUTH_SECRET not set:
Error: AUTH_SECRET environment variable is not set
Generate and set
AUTH_SECRET
in
.env.local
Google OAuth redirect mismatch:
Error: redirect_uri_mismatch
Ensure redirect URI in Google Console matches:
http://localhost:3000/api/auth/callback/google
Session not persisting:
  • Check
    AUTH_URL
    is set correctly
  • Verify cookies are not blocked
  • Ensure
    sessionToken
    cookie is being set (check browser DevTools)
TypeScript errors with session:
  • Extend the
    Session
    and
    JWT
    types using module augmentation
  • Run
    pnpm tsc --noEmit
    to check for type errors
Credentials provider not working:
  • Ensure
    session.strategy
    is set to
    "jwt"
  • Check
    authorize
    function returns correct user object with
    id
    field
  • Verify password hashing/comparison logic
AUTH_SECRET未设置:
Error: AUTH_SECRET environment variable is not set
生成并在
.env.local
中设置
AUTH_SECRET
Google OAuth重定向不匹配:
Error: redirect_uri_mismatch
确保Google控制台中的重定向URI与实际地址一致:
http://localhost:3000/api/auth/callback/google
会话无法持久化:
  • 检查
    AUTH_URL
    是否配置正确
  • 验证浏览器是否阻止了Cookie
  • 确认
    sessionToken
    Cookie已正确设置(查看浏览器开发者工具)
Session相关的TypeScript错误:
  • 使用模块扩展
    Session
    JWT
    类型
  • 运行
    pnpm tsc --noEmit
    检查类型错误
凭证提供器无法工作:
  • 确保
    session.strategy
    设置为
    "jwt"
  • 检查
    authorize
    函数是否返回包含
    id
    字段的正确用户对象
  • 验证密码哈希/比对逻辑是否正确

Resources

更多资源