Next.js项目MindAI教程 - 第四章:用户认证系统

devtools/2025/3/16 20:18:36/

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
  • 实现心理测评功能
  • 开发情绪检测系统
  • 构建在线咨询功能

http://www.ppmy.cn/devtools/167633.html

相关文章

优化Go错误码管理:构建清晰、优雅的HTTP和gRPC错误码规范

在系统开发过程中&#xff0c;如何优雅地管理错误信息一直是个棘手问题。传统的错误处理方式往往存在不统一、难以维护等缺点。而 errcode 模块通过对错误码进行规范化管理&#xff0c;为系统级和业务级错误提供了统一的编码标准。本文将带您深入了解 errcode 的设计原理、错误…

Windows的tftp udp 69端口被占用,通过netstat查询

要用到tftp server传输文件&#xff0c;启动3cdeamon工具时报错&#xff0c;端口被占用 tftp端口为udp 69 查询方法 C:\Users\funny>netstat -ano | findstr :69UDP 0.0.0.0:69 *:* 7664UDP [::]:69 …

运行Clip多模态模型报错:OSError: Can‘t load tokenizer for ‘bert-base-chinese‘

目录 1.OSError 2.解决方法 3.运行存在问题 1.OSError加载模型文件出现 OSError: Can’t load tokenizer for ‘bert-base-chinese’. If you were trying to load it from ‘https://huggingface.co/models’, make sure you don’t have a local directory with the same…

python速通小笔记-------1.容器

1.字符串的标识 字符串需要用“”标识。 与c不同&#xff0c;python 写变量时 不需要标明数据类型每一行最后不需要加&#xff1b; 2.print函数的使用 与c中的printf函数一致 3.运算符 4.字符串str操作 1. 实现字符串拼接 2.% 实现字符串初始化 %s占位会把变量强制转变为…

idea 2023社区版自动生成 serialVersionUID

在 IDEA 2023 社区版中&#xff0c;自动生成serialVersionUID且不使用默认1L的方法如下 打开设置&#xff1a;点击菜单栏中的 “File”&#xff0c;选择 “Settings”。进入检查设置&#xff1a;在弹出的设置窗口中&#xff0c;导航到 “Editor” -> “Inspections”。配置…

【WiFi 7核心技术及未来挑战】

作为刚刚开始从事这一领域的人&#xff0c;浅浅学习了一下WiFi 7&#xff08;IEEE 802.11be&#xff09;。Wi-Fi 7发展迅速&#xff0c;提供前所未有的速度、更低的延迟和更高的可靠性。但从频谱政策到能效挑战&#xff0c;再到成本&#xff0c;仍有许多问题亟待解决。 Wi-Fi 7…

8664蛋糕的美味值

8664蛋糕的美味值 ⭐️难度&#xff1a;中等 &#x1f31f;考点&#xff1a;枚举 &#x1f4d6; &#x1f4da; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in );int n sc.nextInt();int k s…

嵌入式八股,为什么单片机中不使用malloc函数

1. 资源限制 单片机的内存资源通常非常有限&#xff0c;尤其是RAM的大小可能只有几KB到几十KB。在这种情况下&#xff0c;使用 malloc 进行动态内存分配可能会导致内存碎片化&#xff0c;使得程序在运行过程中逐渐耗尽可用内存。 2. 内存碎片问题 malloc 函数在分配和释放内…