关于 authjs-skills
authjs-skills 汇总 Auth.js v5 的配置与使用指南,面向在 Next.js 应用中实现身份验证的开发者。内容涵盖提供方、会话、路由保护、数据库适配器及从 v4 迁移。
主要功能
- 01Auth.js v5 基础配置
- 02Google 与凭据提供方
- 03会话和回调管理
- 04路由保护与角色控制
- 05数据库适配器配置
- 06v4 迁移说明
适用场景
- 01为 Next.js 应用配置登录
- 02接入 OAuth 或用户名密码
- 03保护页面和 API 路由
- 04从 Auth.js v4 升级
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ gocallum/ nextjs16-agent-skills --skill authjs-skills安装后在 Claude Code 中输入/authjs-skills确认已加载。
详细介绍
链接
- 入门指南:https://authjs.dev/getting-started/installation?framework=Next.js
- 迁移到 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 参考:https://authjs.dev/reference/core
- 会话管理:https://authjs.dev/getting-started/session-management
- 概念:https://authjs.dev/concepts
安装
pnpm add next-auth@beta
注意:Auth.js v5 目前处于 beta 阶段。使用 next-auth@beta 安装最新的 v5 版本。
Auth.js v5 有哪些新变化?
相较于 v4 的主要变化
- 简化配置:设置更加精简,并提供更好的 TypeScript 支持
- 通用的
auth()导出:使用单个函数在所有上下文中进行身份验证 - 增强安全性:改进 CSRF 防护和会话处理
- 支持 Edge Runtime:完全兼容 Edge Runtime 和中间件
- 更好的类型安全性:全面改进 TypeScript 定义
环境变量
必需的环境变量
# Auth.js Configuration
AUTH_SECRET=your_secret_key_here
# Google OAuth (if using Google provider)
AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secret
# For production deployments
AUTH_URL=https://yourdomain.com
# For development (optional, defaults to http://localhost:3000)
# AUTH_URL=http://localhost:3000
生成 AUTH_SECRET
# Generate a random secret (Unix/Linux/macOS)
openssl rand -base64 32
# Alternative using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Using pnpm
pnpm dlx auth secret
重要:切勿将 AUTH_SECRET 提交到版本控制中。开发时请使用 .env.local。
基本设置(Next.js App Router)
1. 创建 auth.ts 配置文件
在项目根目录(与 package.json 同级)创建 auth.ts:
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
},
},
})
注意:这是一个基本设置示例。有关可用于生产环境的凭据身份验证,请参阅下方的“Credentials Provider”部分,其中包含使用 bcrypt 进行适当的密码哈希处理以及数据库集成。
2. 创建 API 路由处理程序
创建 app/api/auth/[...nextauth]/route.ts:
import { handlers } from "@/auth"
export const { GET, POST } = handlers
3. 添加中间件(可选,但建议添加)
在项目根目录创建 middleware.ts:
export { auth as middleware } from "@/auth"
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
如需更多控制:
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
1. 设置 Google Cloud Console
- 前往 Google Cloud Console
- 创建新项目或选择现有项目
- 启用 Google+ API
- 创建 OAuth 2.0 凭据:
- 应用类型:Web 应用
- 授权的重定向 URI:
- 开发环境:
http://localhost:3000/api/auth/callback/google - 生产环境:
https://yourdomain.com/api/auth/callback/google
- 开发环境:
- 将 Client ID 和 Client Secret 复制到
.env.local
2. 配置
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 选项
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,
})
Credentials Provider(用户名/密码)
必需的依赖项
# Install required packages for credentials provider
pnpm add bcryptjs zod
pnpm add -D @types/bcryptjs
1. 基本配置
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: "[email protected]" },
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
},
})
2. 用户注册示例
// 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 }
)
}
}
在组件中使用 Auth
服务端组件
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>
)
}
服务端操作
"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")
}
客户端组件(使用 SessionProvider)
// app/providers.tsx
"use client"
import { SessionProvider } from "next-auth/react"
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>
}
// 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>
)
}
// 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>
)
}
登录/登出操作
以编程方式登录
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" })
}
登录表单组件
// 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>
)
}
登出
import { signOut } from "@/auth"
export default function SignOutButton() {
return (
<form
action={async () => {
"use server"
await signOut()
}}
>
<button type="submit">Sign Out</button>
</form>
)
}
会话管理
会话策略
Auth.js v5 支持两种会话策略:
- JWT(默认):将会话存储在加密的 JWT 令牌中
- 数据库:将会话存储在数据库中
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
},
})
扩展会话
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
},
},
})
回调
必要的回调
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
},
},
})
数据库适配器(可选)
如需在数据库中持久化用户、账户和会话,请安装 Prisma 适配器:
pnpm add @auth/prisma-adapter
然后在 auth.ts 中配置:
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
],
})
必需的 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 路由
自定义 API 端点
// 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,
})
}
受保护路由辅助函数
// 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 })
})
}
最佳实践
安全性
- 始终对密码进行哈希处理:使用 bcrypt、argon2 或类似工具
- 在生产环境中使用 HTTPS:安全传输 Cookie 的必要条件
- 验证环境变量:检查 AUTH_SECRET 和 Provider 凭据
- 设置安全的 Cookie 选项:
cookies: { sessionToken: { name: `__Secure-next-auth.session-token`, options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production', }, }, } - 实施速率限制:保护登录端点
- 使用 CSRF 防护:v5 默认启用
- 验证重定向:使用
redirect回调防止开放重定向
会话管理
- 使用适当的 maxAge:默认为 30 天,请根据安全要求进行调整
- 定期更新会话:使用
updateAge刷新会话数据 - 妥善处理会话过期:提供清晰的 UI 提示
- 安全存储会话:对敏感应用使用数据库策略
提供商配置
- Google OAuth:仅请求所需的最少权限范围
- 凭据:始终使用 zod 或类似工具验证输入
- 多个提供商:谨慎允许账户关联
- 提供商专属逻辑:使用回调处理不同提供商之间的差异
性能
- 缓存会话检查:使用中间件保护路由
- 尽量减少数据库调用:适当情况下使用 JWT 策略
- 优化数据库查询:为频繁查询的字段添加索引
- 使用 Edge Runtime:加快中间件中的身份验证检查
类型安全
- 正确扩展类型:使用模块增强添加自定义会话字段
- 验证输入:使用 zod 进行运行时类型检查
- 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).*)"],
}
多提供商设置
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
},
},
})
自定义登录页面
// 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>
)
}
基于角色的访问控制(RBAC)
// 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 } })
}
从 v4 迁移到 v5
主要差异
- 导入变更:
next-auth包保持不变,但导入方式已简化 - 通用
auth():使用auth()替换getServerSession - 中间件:直接将
auth用作中间件 - 配置:配置更加精简,所需选项更少
迁移步骤
// 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()
}
// 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"
故障排除
常见问题
未设置 AUTH_SECRET:
Error: AUTH_SECRET environment variable is not set
在 .env.local 中生成并设置 AUTH_SECRET
Google OAuth 重定向不匹配:
Error: redirect_uri_mismatch
确保 Google Console 中的重定向 URI 与此一致:http://localhost:3000/api/auth/callback/google
会话未持久化:
- 检查是否正确设置了
AUTH_URL - 确认 Cookie 未被阻止
- 确保已设置
sessionTokenCookie(检查浏览器 DevTools)
会话相关的 TypeScript 错误:
- 使用模块增强扩展
Session和JWT类型 - 运行
pnpm tsc --noEmit检查类型错误
Credentials 提供商无法正常工作:
- 确保
session.strategy设置为"jwt" - 检查
authorize函数是否返回包含id字段的正确用户对象 - 验证密码哈希与比较逻辑
资源
评论
支持 Markdown