🙋 需要支持 GitHub、Google 登录?Auth.js(原 NextAuth)让认证变得简单。
安装配置#
pnpm add next-auth@beta🔶 注意:Next.js 15 需要使用 Auth.js v5 (beta)。
基础配置#
创建配置文件#
import NextAuth from 'next-auth'import GitHub from 'next-auth/providers/github'import Google from 'next-auth/providers/google'
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET!, }), ],})环境变量#
AUTH_SECRET="your-auth-secret" # openssl rand -base64 32
# GitHub OAuthGITHUB_ID="your-github-client-id"GITHUB_SECRET="your-github-client-secret"
# Google OAuthGOOGLE_ID="your-google-client-id"GOOGLE_SECRET="your-google-client-secret"API 路由#
import { handlers } from '@/auth'
export const { GET, POST } = handlersOAuth 提供者#
GitHub#
import GitHub from 'next-auth/providers/github'
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, // 自定义授权范围 authorization: { params: { scope: 'read:user user:email', }, }, }), ],})Google#
import Google from 'next-auth/providers/google'
Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET!, authorization: { params: { prompt: 'consent', access_type: 'offline', response_type: 'code', }, },})微信(需要自定义)#
import type { OAuthConfig } from 'next-auth/providers'
export function Wechat(): OAuthConfig<any> { return { id: 'wechat', name: '微信', type: 'oauth', authorization: { url: 'https://open.weixin.qq.com/connect/qrconnect', params: { appid: process.env.WECHAT_APP_ID, scope: 'snsapi_login', }, }, token: 'https://api.weixin.qq.com/sns/oauth2/access_token', userinfo: 'https://api.weixin.qq.com/sns/userinfo', profile(profile) { return { id: profile.unionid || profile.openid, name: profile.nickname, image: profile.headimgurl, } }, clientId: process.env.WECHAT_APP_ID, clientSecret: process.env.WECHAT_APP_SECRET, }}凭证登录#
配置#
import NextAuth from 'next-auth'import Credentials from 'next-auth/providers/credentials'import { compare } from 'bcryptjs'import { db } from '@/lib/db'
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ Credentials({ name: 'credentials', credentials: { email: { label: '邮箱', type: 'email' }, password: { label: '密码', type: 'password' }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) { return null }
const user = await db.user.findUnique({ where: { email: credentials.email as string }, })
if (!user || !user.password) { return null }
const isValid = await compare( credentials.password as string, user.password )
if (!isValid) { return null }
return { id: user.id, email: user.email, name: user.name, image: user.image, } }, }), ], pages: { signIn: '/login', },})登录表单#
'use client'
import { signIn } from 'next-auth/react'import { useState } from 'react'import { useRouter } from 'next/navigation'
export function LoginForm() { const [error, setError] = useState('') const [loading, setLoading] = useState(false) const router = useRouter()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() setLoading(true) setError('')
const formData = new FormData(e.currentTarget)
const result = await signIn('credentials', { email: formData.get('email'), password: formData.get('password'), redirect: false, })
setLoading(false)
if (result?.error) { setError('邮箱或密码错误') } else { router.push('/dashboard') router.refresh() } }
return ( <form onSubmit={handleSubmit} className="space-y-4"> <div> <input name="email" type="email" placeholder="邮箱" required className="w-full border rounded px-3 py-2" /> </div> <div> <input name="password" type="password" placeholder="密码" required className="w-full border rounded px-3 py-2" /> </div>
{error && <p className="text-red-500">{error}</p>}
<button type="submit" disabled={loading} className="w-full bg-blue-600 text-white py-2 rounded" > {loading ? '登录中...' : '登录'} </button> </form> )}数据库适配器#
Prisma 适配器#
pnpm add @auth/prisma-adapterimport NextAuth from 'next-auth'import { PrismaAdapter } from '@auth/prisma-adapter'import { prisma } from '@/lib/prisma'import GitHub from 'next-auth/providers/github'
export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), providers: [ GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ], session: { strategy: 'database', // 使用数据库存储 session },})Prisma Schema#
model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}
model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text 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)}
model VerificationToken { identifier String token String @unique expires DateTime
@@unique([identifier, token])}获取会话#
服务端#
import { auth } from '@/auth'import { redirect } from 'next/navigation'
export default async function DashboardPage() { const session = await auth()
if (!session) { redirect('/login') }
return ( <div> <h1>欢迎,{session.user?.name}</h1> <img src={session.user?.image || ''} alt="头像" /> </div> )}客户端#
'use client'
import { useSession, signOut } from 'next-auth/react'
export function UserMenu() { const { data: session, status } = useSession()
if (status === 'loading') { return <div>加载中...</div> }
if (!session) { return <a href="/login">登录</a> }
return ( <div className="flex items-center gap-4"> <img src={session.user?.image || ''} alt="头像" className="w-8 h-8 rounded-full" /> <span>{session.user?.name}</span> <button onClick={() => signOut()}>退出</button> </div> )}SessionProvider#
'use client'
import { SessionProvider } from 'next-auth/react'
export function Providers({ children }: { children: React.ReactNode }) { return <SessionProvider>{children}</SessionProvider>}import { Providers } from './providers'
export default function RootLayout({ children,}: { children: React.ReactNode}) { return ( <html lang="zh-CN"> <body> <Providers>{children}</Providers> </body> </html> )}回调配置#
自定义 JWT#
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [...], callbacks: { async jwt({ token, user, account }) { // 首次登录时添加用户信息 if (user) { token.id = user.id token.role = user.role }
// 添加 OAuth 访问令牌 if (account) { token.accessToken = account.access_token }
return token }, async session({ session, token }) { // 将 token 信息添加到 session session.user.id = token.id as string session.user.role = token.role as string session.accessToken = token.accessToken as string
return session }, },})类型扩展#
import { DefaultSession } from 'next-auth'
declare module 'next-auth' { interface Session { user: { id: string role: string } & DefaultSession['user'] accessToken?: string }
interface User { role?: string }}
declare module 'next-auth/jwt' { interface JWT { id: string role: string accessToken?: string }}登录回调#
callbacks: { async signIn({ user, account, profile }) { // 限制特定域名邮箱 if (account?.provider === 'google') { return profile?.email?.endsWith('@company.com') ?? false }
// 检查用户是否被禁用 const dbUser = await db.user.findUnique({ where: { email: user.email! }, })
if (dbUser?.banned) { return false }
return true },}中间件保护#
import { auth } from '@/auth'
export default auth((req) => { const isLoggedIn = !!req.auth const isAuthPage = req.nextUrl.pathname.startsWith('/login') const isProtected = req.nextUrl.pathname.startsWith('/dashboard')
if (isProtected && !isLoggedIn) { return Response.redirect(new URL('/login', req.url)) }
if (isAuthPage && isLoggedIn) { return Response.redirect(new URL('/dashboard', req.url)) }})
export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],}自定义登录页#
import { signIn } from '@/auth'
export default function LoginPage() { return ( <div className="min-h-screen flex items-center justify-center"> <div className="max-w-md w-full 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('github', { redirectTo: '/dashboard' }) }} > <button type="submit" className="w-full flex items-center justify-center gap-2 border rounded py-2" > <GitHubIcon /> 使用 GitHub 登录 </button> </form>
<form action={async () => { 'use server' await signIn('google', { redirectTo: '/dashboard' }) }} > <button type="submit" className="w-full flex items-center justify-center gap-2 border rounded py-2" > <GoogleIcon /> 使用 Google 登录 </button> </form> </div>
<div className="relative"> <div className="absolute inset-0 flex items-center"> <div className="w-full border-t" /> </div> <div className="relative flex justify-center text-sm"> <span className="px-2 bg-white text-gray-500">或</span> </div> </div>
{/* 凭证登录表单 */} <CredentialsForm /> </div> </div> )}退出登录#
// 服务端 Actionimport { signOut } from '@/auth'
export default function LogoutButton() { return ( <form action={async () => { 'use server' await signOut({ redirectTo: '/' }) }} > <button type="submit">退出登录</button> </form> )}
// 客户端;('use client')import { signOut } from 'next-auth/react'
export function ClientLogoutButton() { return <button onClick={() => signOut({ callbackUrl: '/' })}>退出登录</button>}常见问题#
🤔 Q: OAuth 回调地址怎么配置?
在 OAuth 提供商后台配置:
开发环境: http://localhost:3000/api/auth/callback/github生产环境: https://yourdomain.com/api/auth/callback/github🤔 Q: 如何同时支持 JWT 和数据库 Session?
session: { strategy: 'jwt', // 或 'database'}JWT 更适合 serverless,数据库 Session 更易管理。
🤔 Q: 如何刷新 OAuth Access Token?
使用 @auth/core 的 rotation 功能或在 jwt 回调中处理。
下一篇将介绍环境变量与配置,学习如何安全管理应用配置。
-EOF-