authjs-skills
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLinks
相关链接
- Getting Started: https://authjs.dev/getting-started/installation?framework=Next.js
- Migrating to v5: https://authjs.dev/getting-started/migrating-to-v5
- Google Provider: https://authjs.dev/getting-started/providers/google
- Credentials Provider: https://authjs.dev/getting-started/providers/credentials
- Core API Reference: https://authjs.dev/reference/core
- Session Management: https://authjs.dev/getting-started/session-management
- Concepts: https://authjs.dev/concepts
- 快速开始:https://authjs.dev/getting-started/installation?framework=Next.js
- 迁移至v5:https://authjs.dev/getting-started/migrating-to-v5
- Google提供器:https://authjs.dev/getting-started/providers/google
- 凭证提供器:https://authjs.dev/getting-started/providers/credentials
- 核心API参考:https://authjs.dev/reference/core
- 会话管理:https://authjs.dev/getting-started/session-management
- 核心概念:https://authjs.dev/concepts
Installation
安装步骤
sh
pnpm add next-auth@betaNote: Auth.js v5 is currently in beta. Use to install the latest v5 version.
next-auth@betash
pnpm add next-auth@beta注意:Auth.js v5目前处于测试版。请使用安装最新的v5版本。
next-auth@betaWhat's New in Auth.js v5?
Auth.js v5的新特性
Key Changes from v4
与v4版本的主要变化
- Simplified Configuration: More streamlined setup with better TypeScript support
- Universal Export: Single function for authentication across all contexts
auth() - 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
undefinedenv
undefinedAuth.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
生产环境部署
AUTH_URL=https://yourdomain.com
AUTH_URL=https://yourdomain.com
For development (optional, defaults to http://localhost:3000)
开发环境(可选,默认值为http://localhost:3000)
AUTH_URL=http://localhost:3000
AUTH_URL=http://localhost:3000
undefinedundefinedGenerating AUTH_SECRET
生成AUTH_SECRET
sh
undefinedsh
undefinedGenerate 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
auth.ts1. 创建auth.ts
配置文件
auth.tsCreate at the project root (next to ):
auth.tspackage.jsontypescript
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.jsonauth.tstypescript
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.tstypescript
import { handlers } from "@/auth"
export const { GET, POST } = handlers创建:
app/api/auth/[...nextauth]/route.tstypescript
import { handlers } from "@/auth"
export const { GET, POST } = handlers3. Add Middleware (Optional but Recommended)
3. 添加中间件(可选但推荐)
Create at the project root:
middleware.tstypescript
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.tstypescript
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控制台设置
- Go to Google Cloud Console
- Create a new project or select existing
- Enable Google+ API
- 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
- Development:
- Copy Client ID and Client Secret to
.env.local
- 访问Google Cloud控制台
- 创建新项目或选择现有项目
- 启用Google+ API
- 创建OAuth 2.0凭证:
- 应用类型:Web应用
- 授权重定向URI:
- 开发环境:
http://localhost:3000/api/auth/callback/google - 生产环境:
https://yourdomain.com/api/auth/callback/google
- 开发环境:
- 将客户端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
undefinedsh
undefinedInstall required packages for credentials provider
安装凭证提供器所需的包
pnpm add bcryptjs zod
pnpm add -D @types/bcryptjs
undefinedpnpm add bcryptjs zod
pnpm add -D @types/bcryptjs
undefined1. 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:
- JWT (Default): Stores session in encrypted JWT token
- 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支持两种会话策略:
- JWT(默认):将会话存储在加密的JWT令牌中
- 数据库:将会话存储在数据库中
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-adapterThen configure it in your :
auth.tstypescript
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.tstypescript
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 callback to prevent open redirects
redirect
- 始终哈希密码:使用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 to refresh session data
updateAge - 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
主要差异
- Import changes: package remains the same, but imports are simplified
next-auth - Universal : Replace
auth()withgetServerSessionauth() - Middleware: Use as middleware directly
auth - Configuration: More streamlined, fewer options needed
- 导入变化:包名称不变,但导入方式更简洁
next-auth - 通用:用
auth()替代auth()getServerSession - 中间件:直接使用作为中间件
auth - 配置:更简化,所需选项更少
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 setGenerate and set in
AUTH_SECRET.env.localGoogle OAuth redirect mismatch:
Error: redirect_uri_mismatchEnsure redirect URI in Google Console matches:
http://localhost:3000/api/auth/callback/googleSession not persisting:
- Check is set correctly
AUTH_URL - Verify cookies are not blocked
- Ensure cookie is being set (check browser DevTools)
sessionToken
TypeScript errors with session:
- Extend the and
Sessiontypes using module augmentationJWT - Run to check for type errors
pnpm tsc --noEmit
Credentials provider not working:
- Ensure is set to
session.strategy"jwt" - Check function returns correct user object with
authorizefieldid - Verify password hashing/comparison logic
AUTH_SECRET未设置:
Error: AUTH_SECRET environment variable is not set生成并在中设置
.env.localAUTH_SECRETGoogle OAuth重定向不匹配:
Error: redirect_uri_mismatch确保Google控制台中的重定向URI与实际地址一致:
http://localhost:3000/api/auth/callback/google会话无法持久化:
- 检查是否配置正确
AUTH_URL - 验证浏览器是否阻止了Cookie
- 确认Cookie已正确设置(查看浏览器开发者工具)
sessionToken
Session相关的TypeScript错误:
- 使用模块扩展和
Session类型JWT - 运行检查类型错误
pnpm tsc --noEmit
凭证提供器无法工作:
- 确保设置为
session.strategy"jwt" - 检查函数是否返回包含
authorize字段的正确用户对象id - 验证密码哈希/比对逻辑是否正确
Resources
更多资源
- Official Docs: https://authjs.dev
- GitHub: https://github.com/nextauthjs/next-auth
- Discord Community: https://discord.gg/nextauth
- Examples: https://github.com/nextauthjs/next-auth/tree/main/apps/examples
- Provider List: https://authjs.dev/getting-started/providers