1. NextAuth.js 集成
1.1 安装依赖
npm install next-auth bcryptjs
npm install @types/bcryptjs --save-dev
npm install next-auth bcryptjs
npm install @types/bcryptjs --save-dev
1.2 配置NextAuth
// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { compare } from 'bcryptjs'
import { UserService } from '@/services/database/user.service'const handler = NextAuth({providers: [CredentialsProvider({name: 'Credentials',credentials: {email: { label: "邮箱", type: "email" },password: { label: "密码", type: "password" }},async authorize(credentials) {if (!credentials?.email || !credentials?.password) {throw new Error('请输入邮箱和密码')}const user = await UserService.findByEmail(credentials.email)if (!user) {throw new Error('用户不存在')}const isValid = await compare(credentials.password, user.password)if (!isValid) {throw new Error('密码错误')}return {id: user.id,email: user.email,name: user.name,role: user.role,}}})],session: {strategy: 'jwt',},pages: {signIn: '/auth/login',signOut: '/auth/logout',error: '/auth/error',},callbacks: {async jwt({ token, user }) {if (user) {token.role = user.roletoken.id = user.id}return token},async session({ session, token }) {if (session.user) {session.user.role = token.role as stringsession.user.id = token.id as string}return session}}
})export { handler as GET, handler as POST }
2. 认证页面实现
2.1 登录页面
// src/app/auth/login/page.tsx
'use client'import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'export default function LoginPage() {const router = useRouter()const [error, setError] = useState('')const [loading, setLoading] = useState(false)async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {e.preventDefault()setLoading(true)setError('')const formData = new FormData(e.currentTarget)const email = formData.get('email') as stringconst password = formData.get('password') as stringtry {const result = await signIn('credentials', {redirect: false,email,password,})if (result?.error) {setError(result.error)} else {router.push('/')router.refresh()}} catch (error) {setError('登录失败,请重试')} finally {setLoading(false)}}return (<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"><div className="max-w-md w-full space-y-8"><div><h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">登录账户</h2></div><form className="mt-8 space-y-6" onSubmit={handleSubmit}>{error && (<div className="rounded-md bg-red-50 p-4"><div className="text-sm text-red-700">{error}</div></div>)}<div className="rounded-md shadow-sm -space-y-px"><div><label htmlFor="email" className="sr-only">邮箱</label><inputid="email"name="email"type="email"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"placeholder="邮箱地址"/></div><div><label htmlFor="password" className="sr-only">密码</label><inputid="password"name="password"type="password"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"placeholder="密码"/></div></div><div><buttontype="submit"disabled={loading}className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{loading ? '登录中...' : '登录'}</button></div><div className="text-sm text-center"><Linkhref="/auth/register"className="font-medium text-primary-600 hover:text-primary-500">还没有账户?立即注册</Link></div></form></div></div>)
}
2.2 注册页面
// src/app/auth/register/page.tsx
'use client'import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'export default function RegisterPage() {const router = useRouter()const [error, setError] = useState('')const [loading, setLoading] = useState(false)async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {e.preventDefault()setLoading(true)setError('')const formData = new FormData(e.currentTarget)const data = {email: formData.get('email') as string,password: formData.get('password') as string,name: formData.get('name') as string,}try {const res = await fetch('/api/users', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(data),})if (!res.ok) {const error = await res.json()throw new Error(error.message || '注册失败')}router.push('/auth/login')} catch (error) {setError(error instanceof Error ? error.message : '注册失败,请重试')} finally {setLoading(false)}}return (<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"><div className="max-w-md w-full space-y-8"><div><h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">创建新账户</h2></div><form className="mt-8 space-y-6" onSubmit={handleSubmit}>{error && (<div className="rounded-md bg-red-50 p-4"><div className="text-sm text-red-700">{error}</div></div>)}<div className="rounded-md shadow-sm -space-y-px"><div><label htmlFor="name" className="sr-only">姓名</label><inputid="name"name="name"type="text"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"placeholder="姓名"/></div><div><label htmlFor="email" className="sr-only">邮箱</label><inputid="email"name="email"type="email"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"placeholder="邮箱地址"/></div><div><label htmlFor="password" className="sr-only">密码</label><inputid="password"name="password"type="password"requiredclassName="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"placeholder="密码"/></div></div><div><buttontype="submit"disabled={loading}className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{loading ? '注册中...' : '注册'}</button></div><div className="text-sm text-center"><Linkhref="/auth/login"className="font-medium text-primary-600 hover:text-primary-500">已有账户?立即登录</Link></div></form></div></div>)
}
3. 权限控制实现
3.1 创建权限中间件
// src/middleware.ts
import { withAuth } from 'next-auth/middleware'
import { NextResponse } from 'next/server'export default withAuth(function middleware(req) {const token = req.nextauth.tokenconst path = req.nextUrl.pathname// 管理员路由保护if (path.startsWith('/admin') && token?.role !== 'ADMIN') {return NextResponse.redirect(new URL('/auth/login', req.url))}// 咨询师路由保护if (path.startsWith('/counselor') && token?.role !== 'COUNSELOR') {return NextResponse.redirect(new URL('/auth/login', req.url))}return NextResponse.next()},{callbacks: {authorized: ({ token }) => !!token},}
)export const config = {matcher: ['/admin/:path*', '/counselor/:path*', '/profile/:path*']
}
3.2 创建权限Hook
// src/hooks/useAuth.ts
'use client'import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'export function useAuth(requiredRole?: string) {const { data: session, status } = useSession()const router = useRouter()useEffect(() => {if (status === 'loading') returnif (!session) {router.push('/auth/login')return}if (requiredRole && session.user.role !== requiredRole) {router.push('/')}}, [session, status, requiredRole, router])return { session, status }
}
3.3 创建受保护的组件包装器
// src/components/auth/ProtectedRoute.tsx
'use client'import { useAuth } from '@/hooks/useAuth'
import { LoadingSpinner } from '@/components/ui/LoadingSpinner'interface ProtectedRouteProps {children: React.ReactNoderequiredRole?: string
}export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {const { status } = useAuth(requiredRole)if (status === 'loading') {return (<div className="min-h-screen flex items-center justify-center"><LoadingSpinner /></div>)}return <>{children}</>
}
4. 用户状态管理
4.1 创建用户状态Store
// src/store/useUserStore.ts
import { create } from 'zustand'
import { UserProfile } from '@/types'interface UserState {profile: UserProfile | nullsetProfile: (profile: UserProfile | null) => voidupdateProfile: (data: Partial<UserProfile>) => void
}export const useUserStore = create<UserState>((set) => ({profile: null,setProfile: (profile) => set({ profile }),updateProfile: (data) =>set((state) => ({profile: state.profile ? { ...state.profile, ...data } : null,})),
}))
4.2 创建用户Provider
// src/components/providers/UserProvider.tsx
'use client'import { useSession } from 'next-auth/react'
import { useEffect } from 'react'
import { useUserStore } from '@/store/useUserStore'export function UserProvider({ children }: { children: React.ReactNode }) {const { data: session } = useSession()const setProfile = useUserStore((state) => state.setProfile)useEffect(() => {if (session?.user) {// 获取用户详细信息fetch(`/api/users/profile?userId=${session.user.id}`).then((res) => res.json()).then((data) => {setProfile(data)}).catch(console.error)} else {setProfile(null)}}, [session, setProfile])return <>{children}</>
}
5.集成到应用
5.1更新根布局
// src/app/layout.tsx
import { Inter } from 'next/font/google'
import { SessionProvider } from 'next-auth/react'
import { UserProvider } from '@/components/providers/UserProvider'
import PageLayout from '@/components/layout/PageLayout'
import './globals.css'const inter = Inter({ subsets: ['latin'] })export default function RootLayout({children,
}: {children: React.ReactNode
}) {return (<html lang="zh-CN"><body className={inter.className}><SessionProvider><UserProvider><PageLayout>{children}</PageLayout></UserProvider></SessionProvider></body></html>)
}
5.2更新导航栏
// src/components/layout/Navbar.tsx
'use client'import { useSession, signOut } from 'next-auth/react'
import Link from 'next/link'
import { useUserStore } from '@/store/useUserStore'export default function Navbar() {const { data: session } = useSession()const profile = useUserStore((state) => state.profile)return (<nav className="bg-white shadow">{/* ... 其他导航代码 ... */}<div className="flex items-center">{session ? (<div className="relative ml-3"><div className="flex items-center"><span className="text-gray-700 mr-4">{profile?.name || session.user.email}</span><buttononClick={() => signOut()}className="text-gray-600 hover:text-gray-900">退出</button></div></div>) : (<Linkhref="/auth/login"className="text-gray-600 hover:text-gray-900">登录</Link>)}</div></nav>)
}
6. 下一步计划
- 集成OpenAI API
- 实现心理测评功能
- 开发情绪检测系统
- 构建在线咨询功能