Next.js + Prisma + Auth.js 实现完整的认证方案

前言

在现代 Web 应用中,用户认证是一个基础且重要的功能。本文将介绍如何使用 Next.js + Prisma + Auth.js 实现一个完整的认证方案。这个方案既安全又灵活,能满足大多数项目需求。

技术栈选择

  • • Next.js: React 全栈框架,提供了服务端渲染和 API 路由
  • • Prisma: 现代数据库 ORM,简化数据库操作
  • • Auth.js: 专注于认证的库,支持多种认证方式

这个组合的优势在于:Next.js 的全栈特性让前后端代码共处一个项目,Prisma 的类型安全让数据库操作更可靠,Auth.js 则提供了完整的认证解决方案。

实现步骤

1. 数据库设计

为什么选择这样的用户表结构?

用户表(User)是认证系统的核心,我们的设计考虑了以下几个关键点:

  • • id: 使用 cuid() 而不是自增ID,因为它提供了更好的安全性和分布式唯一性
  • • email: 设为唯一索引,作为用户的主要标识符
  • • password: 只存储加密后的密码哈希
  • • role: 用于基本的权限控制
  • • emailVerified: 支持邮箱验证功能
  • • createdAt/updatedAt: 用于审计和数据跟踪
// schema.prisma
datasource db {provider = "postgresql" // 或 "mysql"url      = env("DATABASE_URL")
}generator client {provider = "prisma-client-js"
}model User {id            String    @id @default(cuid())name          String?email         String    @uniquepassword      Stringrole          String    @default("user")emailVerified DateTime?image         String?createdAt     DateTime  @default(now())updatedAt     DateTime  @updatedAt
}

2. Auth.js 配置

Auth.js 工作原理

Auth.js(原 NextAuth.js)提供了一个完整的认证框架,它的工作流程是:

  1. 1. 用户提交登录请求
  2. 2. Auth.js 通过配置的 Provider 验证凭据
  3. 3. 验证成功后创建 JWT token
  4. 4. 将 token 存储在 cookie 中
  5. 5. 后续请求通过 middleware 验证 token

下面是具体配置及解释:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';const prisma = new PrismaClient();export const authOptions = {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 prisma.user.findUnique({where: { email: credentials.email }});if (!user) {throw new Error('用户不存在');}const isPasswordValid = await bcrypt.compare(credentials.password,user.password);if (!isPasswordValid) {throw new Error('密码错误');}return {id: user.id,email: user.email,name: user.name,role: user.role,};}})],callbacks: {async jwt({ token, user }) {if (user) {token.role = user.role;}return token;},async session({ session, token }) {session.user.role = token.role;return session;}},pages: {signIn: '/login',error: '/auth/error',},session: {strategy: "jwt",maxAge: 30 * 24 * 60 * 60, // 30 天},secret: process.env.NEXTAUTH_SECRET,
};const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
配置详解:
  1. 1. Providers配置
  • • CredentialsProvider: 处理用户名密码登录
  • • 可以添加其他提供商如 Google, GitHub 等
  1. 1. Callbacks配置
  • • jwt: 自定义 JWT token 内容
  • • session: 定制返回给客户端的会话信息
  1. 1. Session配置
  • • strategy: "jwt" 使用无状态JWT存储
  • • maxAge: 控制会话有效期

3. API 实现

注册流程设计

注册API需要处理的关键点:

  1. 1. 输入验证
  2. 2. 密码加密
  3. 3. 用户查重
  4. 4. 安全响应
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { NextResponse } from 'next/server';const prisma = new PrismaClient();export async function POST(request) {try {const { email, password, name } = await request.json();// 验证输入if (!email || !password) {return NextResponse.json({ error: '请提供所有必需的信息' },{ status: 400 });}// 检查用户是否已存在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,password: hashedPassword,name,}});return NextResponse.json({ message: '注册成功', user: { id: user.id, email: user.email, name: user.name } },{ status: 201 });} catch (error) {return NextResponse.json({ error: '注册失败' },{ status: 500 });}
}
安全考虑
  • • 密码通过 bcrypt 加密,使用10轮加密(性能和安全的平衡)
  • • 永不返回密码相关信息
  • • 统一的错误处理避免信息泄露

4. 中间件实现

中间件的作用

Next.js 中间件在请求到达页面之前执行,用于:

  1. 1. 路由保护
  2. 2. 权限验证
  3. 3. 重定向处理
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";export default withAuth(function middleware(req) {// 获取当前路径和用户角色const path = req.nextUrl.pathname;const token = req.nextauth.token;// 管理员路由保护if (path.startsWith("/admin") && token?.role !== "admin") {return NextResponse.redirect(new URL("/login", req.url));}return NextResponse.next();},{callbacks: {authorized: ({ token }) => !!token},}
);// 配置需要保护的路由
export const config = {matcher: ["/dashboard/:path*", "/admin/:path*", "/profile/:path*"]
};
工作流程
  1. 1. 请求进入
  2. 2. 中间件检查 token
  3. 3. 根据路由规则决定:
    • • 允许访问
    • • 重定向到登录
    • • 返回错误

5. 前端实现

登录组件设计考虑

登录表单需要处理:

  1. 1. 表单状态管理
  2. 2. 错误处理
  3. 3. 加载状态
  4. 4. 成功后跳转
'use client';import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';export default function LoginPage() {const router = useRouter();const [error, setError] = useState('');const [loading, setLoading] = useState(false);async function handleSubmit(e) {e.preventDefault();setLoading(true);setError('');const formData = new FormData(e.currentTarget);const email = formData.get('email');const password = formData.get('password');try {const result = await signIn('credentials', {email,password,redirect: false,});if (result?.error) {setError(result.error);} else {router.push('/dashboard');router.refresh();}} catch (error) {setError('登录过程中出现错误');} finally {setLoading(false);}}return (<div className="min-h-screen flex items-center justify-center"><form onSubmit={handleSubmit} className="space-y-4 w-full max-w-md"><h1 className="text-2xl font-bold text-center">登录</h1>{error && (<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">{error}</div>)}<div><label htmlFor="email" className="block text-sm font-medium">邮箱</label><inputtype="email"name="email"requiredclassName="mt-1 block w-full rounded-md border-gray-300 shadow-sm"/></div><div><label htmlFor="password" className="block text-sm font-medium">密码</label><inputtype="password"name="password"requiredclassName="mt-1 block w-full rounded-md border-gray-300 shadow-sm"/></div><buttontype="submit"disabled={loading}className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">{loading ? '登录中...' : '登录'}</button></form></div>);
}
用户体验优化
  • • 表单验证即时反馈
  • • 登录状态清晰展示
  • • 错误信息友好展示
  • • 防止重复提交

关键实现细节

密码处理

1. 密码加密存储

使用 bcrypt 进行密码加密是业界标准做法:

// src/lib/auth/password.js
import bcrypt from 'bcryptjs';export async function hashPassword(password) {// 使用10轮加密,在安全性和性能间取得平衡return await bcrypt.hash(password, 10);
}export async function verifyPassword(password, hashedPassword) {return await bcrypt.compare(password, hashedPassword);
}
2. 密码安全策略

实现密码强度验证:

// src/lib/auth/validation.js
export function validatePassword(password) {const minLength = 8;const hasUpperCase = /[A-Z]/.test(password);const hasLowerCase = /[a-z]/.test(password);const hasNumbers = /\d/.test(password);const hasSpecialChar = /[!@#$%^&*]/.test(password);const errors = [];if (password.length < minLength) {errors.push('密码长度至少8位');}if (!hasUpperCase) errors.push('需包含大写字母');if (!hasLowerCase) errors.push('需包含小写字母');if (!hasNumbers) errors.push('需包含数字');if (!hasSpecialChar) errors.push('需包含特殊字符');return {isValid: errors.length === 0,errors};
}

会话管理

1. JWT 配置

配置 JWT 令牌的生成和验证:

// src/app/api/auth/[...nextauth]/route.js 中的配置补充
export const authOptions = {// ... 其他配置jwt: {maxAge: 60 * 60 * 24 * 30, // 30天async encode({ secret, token }) {// 可以添加自定义加密逻辑return jwt.sign(token, secret);},async decode({ secret, token }) {// 可以添加自定义解密逻辑return jwt.verify(token, secret);}},// 添加会话刷新逻辑callbacks: {async jwt({ token, user, account }) {// 初次登录if (account && user) {return {...token,accessToken: account.access_token,refreshToken: account.refresh_token,};}// 检查token是否过期if (Date.now() < token.accessTokenExpires) {return token;}// 刷新tokenreturn refreshAccessToken(token);}}
};
2. 会话刷新实现
// src/lib/auth/session.js
async function refreshAccessToken(token) {try {// 实现token刷新逻辑const response = await fetch('/api/auth/refresh', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({refreshToken: token.refreshToken,}),});const refreshedTokens = await response.json();if (!response.ok) {throw refreshedTokens;}return {...token,accessToken: refreshedTokens.access_token,refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,};} catch (error) {return {...token,error: "RefreshAccessTokenError",};}
}

安全考虑

1. CSRF 防护

实现 CSRF token 验证:

// src/middleware.ts 中添加CSRF检查
import { csrf } from '@/lib/csrf';export async function middleware(request: NextRequest) {// 检查是否是修改数据的请求if (['POST', 'PUT', 'DELETE'].includes(request.method)) {const csrfToken = request.headers.get('X-CSRF-Token');const cookieToken = request.cookies.get('csrf-token');if (!csrfToken || !cookieToken || csrfToken !== cookieToken) {return new NextResponse(JSON.stringify({ message: 'Invalid CSRF token' }),{ status: 403 });}}return NextResponse.next();
}
2. XSS 防护

实现内容安全策略(CSP):

// src/middleware.ts 中添加CSP头
export function middleware(request: NextRequest) {const response = NextResponse.next();// 设置CSP头response.headers.set('Content-Security-Policy',"default-src 'self'; " +"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +"style-src 'self' 'unsafe-inline'; " +"img-src 'self' data: https:; " +"font-src 'self';");return response;
}
3. 登录频率限制

实现请求限流:

// src/lib/auth/rateLimit.js
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';export const loginLimiter = rateLimit({store: new RedisStore({// Redis配置}),windowMs: 15 * 60 * 1000, // 15分钟max: 5, // 限制5次尝试message: {error: '登录尝试次数过多,请15分钟后再试'}
});// 在登录API中使用
export async function POST(request) {try {await loginLimiter(request);// 处理登录逻辑} catch (error) {if (error.message.includes('登录尝试次数过多')) {return NextResponse.json({ error: error.message }, { status: 429 });}throw error;}
}

错误处理

统一错误处理器

创建全局错误处理:

// src/lib/errors/handler.js
export class AuthError extends Error {constructor(message, code = 'AUTH_ERROR') {super(message);this.code = code;}
}export function handleAuthError(error) {console.error(`认证错误: ${error.message}`);// 根据错误类型返回适当的响应const errorResponses = {'AUTH_ERROR': { status: 401, message: '认证失败' },'INVALID_CREDENTIALS': { status: 401, message: '用户名或密码错误' },'USER_NOT_FOUND': { status: 404, message: '用户不存在' },'RATE_LIMIT': { status: 429, message: '请求过于频繁' },// ... 其他错误类型};const response = errorResponses[error.code] || {status: 500,message: '服务器内部错误'};return NextResponse.json({ error: response.message },{ status: response.status });
}

最佳实践

1. 错误处理

a) 统一错误处理中心

创建统一的错误处理服务:

// src/lib/errors/ErrorService.js
export class ErrorService {static async handleError(error, type = 'GENERAL') {// 记录错误await this.logError(error, type);// 返回用户友好的错误信息return this.getUserFriendlyError(error, type);}static async logError(error, type) {// 可以集成 Sentry 或其他日志服务console.error(`[${type}] ${new Date().toISOString()}:`, {message: error.message,stack: error.stack,type,});}static getUserFriendlyError(error, type) {const errorMessages = {AUTH: {default: '认证过程中出现错误',invalid_credentials: '用户名或密码错误',account_locked: '账户已被锁定,请联系管理员',},VALIDATION: {default: '输入数据有误',invalid_email: '请输入有效的邮箱地址',weak_password: '密码强度不足',},// ... 其他错误类型};return errorMessages[type]?.[error.code] || errorMessages[type]?.default || '操作失败,请稍后重试';}
}
b) 前端错误展示组件
// src/components/ErrorDisplay.js
export function ErrorDisplay({ error, onRetry }) {return (<div className="rounded-md bg-red-50 p-4"><div className="flex"><div className="flex-shrink-0"><XCircleIcon className="h-5 w-5 text-red-400" /></div><div className="ml-3"><h3 className="text-sm font-medium text-red-800">{error.message}</h3>{onRetry && (<buttononClick={onRetry}className="mt-2 text-sm text-red-600 hover:text-red-500">重试</button>)}</div></div></div>);
}

2. 性能优化

a) 数据库优化

在 Prisma schema 中添加必要的索引:

model User {id            String    @id @default(cuid())email         String    @uniquepassword      String// ... 其他字段@@index([email]) // 为常用查询字段添加索引
}
b) 缓存策略实现
// src/lib/cache/cacheService.js
import { Redis } from 'ioredis';export class CacheService {constructor() {this.redis = new Redis(process.env.REDIS_URL);}async get(key) {const value = await this.redis.get(key);return value ? JSON.parse(value) : null;}async set(key, value, expireSeconds = 3600) {await this.redis.set(key,JSON.stringify(value),'EX',expireSeconds);}async invalidate(key) {await this.redis.del(key);}
}// 使用示例
const cacheService = new CacheService();async function getUserProfile(userId) {const cacheKey = `user:${userId}:profile`;// 尝试从缓存获取const cached = await cacheService.get(cacheKey);if (cached) return cached;// 缓存未命中,从数据库获取const profile = await prisma.user.findUnique({where: { id: userId }});// 存入缓存await cacheService.set(cacheKey, profile);return profile;
}
c) 组件优化
// src/components/LazyLoadedComponent.js
import dynamic from 'next/dynamic';
import { Suspense } from 'react';// 懒加载组件
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {loading: () => <div>加载中...</div>,ssr: false // 如果不需要服务端渲染
});// 使用 Suspense 包装
export function OptimizedComponent() {return (<Suspense fallback={<div>加载中...</div>}><HeavyComponent /></Suspense>);
}

3. 用户体验

a) 表单状态管理
// src/hooks/useForm.js
import { useState } from 'react';export function useForm(initialValues = {}) {const [values, setValues] = useState(initialValues);const [errors, setErrors] = useState({});const [isSubmitting, setIsSubmitting] = useState(false);const handleChange = (e) => {const { name, value } = e.target;setValues(prev => ({...prev,[name]: value}));// 清除该字段的错误setErrors(prev => ({...prev,[name]: undefined}));};const validate = (values) => {const errors = {};// 添加验证规则if (!values.email) {errors.email = '请输入邮箱';}if (!values.password) {errors.password = '请输入密码';}return errors;};const handleSubmit = async (onSubmit) => {setIsSubmitting(true);const validationErrors = validate(values);if (Object.keys(validationErrors).length === 0) {try {await onSubmit(values);} catch (error) {setErrors({ submit: error.message });}} else {setErrors(validationErrors);}setIsSubmitting(false);};return {values,errors,isSubmitting,handleChange,handleSubmit};
}
b) 记住登录状态实现
// src/lib/auth/rememberMe.js
export const rememberMeOptions = {// 在 Auth.js 配置中使用session: {strategy: "jwt",maxAge: 30 * 24 * 60 * 60, // 30 天},callbacks: {async jwt({ token, user, account, profile, isNewUser }) {if (user) {token.rememberMe = user.rememberMe;}return token;},async session({ session, token }) {session.rememberMe = token.rememberMe;return session;},},
};// 在登录组件中使用
function LoginForm() {const [rememberMe, setRememberMe] = useState(false);const handleLogin = async (credentials) => {await signIn('credentials', {...credentials,rememberMe,callbackUrl: '/dashboard'});};return (<form>{/* 其他表单字段 */}<div className="flex items-center"><inputtype="checkbox"id="remember-me"checked={rememberMe}onChange={(e) => setRememberMe(e.target.checked)}/><label htmlFor="remember-me" className="ml-2">记住我</label></div></form>);
}

扩展功能

1. OAuth 社交登录

a) 配置社交登录提供商
// src/app/api/auth/[...nextauth]/route.js
import GoogleProvider from "next-auth/providers/google";
import GithubProvider from "next-auth/providers/github";export const authOptions = {providers: [GoogleProvider({clientId: process.env.GOOGLE_ID,clientSecret: process.env.GOOGLE_SECRET,}),GithubProvider({clientId: process.env.GITHUB_ID,clientSecret: process.env.GITHUB_SECRET,}),// ... 其他提供商],callbacks: {async signIn({ user, account, profile }) {// 处理首次社交登录if (account.provider === "google" || account.provider === "github") {const existingUser = await prisma.user.findUnique({where: { email: user.email }});if (!existingUser) {// 创建新用户await prisma.user.create({data: {email: user.email,name: user.name,image: user.image,// 设置社交登录特定字段provider: account.provider,providerId: account.providerAccountId,}});}}return true;}}
};
b) 社交登录按钮组件
// src/components/SocialLogin.js
export function SocialLoginButtons() {return (<div className="space-y-3"><buttononClick={() => signIn('google')}className="w-full flex items-center justify-center gap-2 bg-white border border-gray-300 rounded-lg px-4 py-2"><GoogleIcon />使用 Google 登录</button><buttononClick={() => signIn('github')}className="w-full flex items-center justify-center gap-2 bg-gray-800 text-white rounded-lg px-4 py-2"><GithubIcon />使用 GitHub 登录</button></div>);
}

2. 双因素认证(2FA)

a) 2FA 配置
// src/lib/auth/twoFactor.js
import { authenticator } from 'otplib';
import QRCode from 'qrcode';export class TwoFactorService {// 生成密钥static generateSecret() {return authenticator.generateSecret();}// 生成 QR 码 URLstatic async generateQRCode(email, secret) {const otpauth = authenticator.keyuri(email,'YourApp',secret);return await QRCode.toDataURL(otpauth);}// 验证码验证static verifyToken(token, secret) {return authenticator.verify({token,secret});}
}
b) 2FA 设置页面
// src/app/settings/2fa/page.js
'use client';export default function TwoFactorSetupPage() {const [secret, setSecret] = useState('');const [qrCode, setQrCode] = useState('');async function setupTwoFactor() {const response = await fetch('/api/auth/2fa/setup', {method: 'POST'});const { secret, qrCode } = await response.json();setSecret(secret);setQrCode(qrCode);}return (<div><h2>设置双因素认证</h2><button onClick={setupTwoFactor}>开始设置</button>{qrCode && (<div><img src={qrCode} alt="2FA QR Code" /><p>请使用认证器 App 扫描此二维码</p></div>)}</div>);
}

3. 权限管理

a) 角色定义
// src/types/auth.ts
export enum UserRole {USER = 'user',ADMIN = 'admin',EDITOR = 'editor'
}export interface Permission {action: string;subject: string;
}export const rolePermissions: Record<UserRole, Permission[]> = {[UserRole.USER]: [{ action: 'read', subject: 'posts' },{ action: 'create', subject: 'comments' }],[UserRole.EDITOR]: [{ action: 'read', subject: 'posts' },{ action: 'create', subject: 'posts' },{ action: 'update', subject: 'posts' }],[UserRole.ADMIN]: [{ action: 'manage', subject: 'all' }]
};
b) 权限检查中间件
// src/middleware/checkPermission.ts
import { createMiddlewareDecorator } from '@mantine/next';
import { UserRole, rolePermissions } from '@/types/auth';export const checkPermission = createMiddlewareDecorator(async (req, res, next) => {const session = await getServerSession(req);const userRole = session?.user?.role as UserRole;if (!userRole) {return res.status(403).json({ error: '未授权访问' });}const permissions = rolePermissions[userRole];const requiredPermission = {action: req.method.toLowerCase(),subject: req.url.split('/').pop()};const hasPermission = permissions.some(p => (p.action === 'manage' && p.subject === 'all') ||(p.action === requiredPermission.action &&p.subject === requiredPermission.subject));if (!hasPermission) {return res.status(403).json({ error: '权限不足' });}return next();}
);

4. 密码重置

a) 重置密码流程
// src/app/api/auth/reset-password/route.js
export async function POST(request) {const { email } = await request.json();// 生成重置令牌const resetToken = crypto.randomBytes(32).toString('hex');const resetTokenExpiry = new Date(Date.now() + 3600000); // 1小时后过期// 更新用户记录await prisma.user.update({where: { email },data: {resetToken,resetTokenExpiry}});// 发送重置邮件await sendResetEmail(email, resetToken);return NextResponse.json({ message: '重置链接已发送到邮箱' });
}
b) 重置密码邮件服务
// src/lib/email/resetPassword.js
import { createTransport } from 'nodemailer';export async function sendResetEmail(email, token) {const transporter = createTransport({host: process.env.SMTP_HOST,port: process.env.SMTP_PORT,auth: {user: process.env.SMTP_USER,pass: process.env.SMTP_PASS}});const resetUrl = `${process.env.NEXT_PUBLIC_URL}/reset-password?token=${token}`;await transporter.sendMail({from: '"YourApp" <noreply@yourapp.com>',to: email,subject: '密码重置请求',html: `<h1>密码重置</h1><p>点击下面的链接重置密码:</p><a href="${resetUrl}">${resetUrl}</a><p>此链接1小时内有效</p>`});
}
c) 重置密码页面
// src/app/reset-password/page.js
'use client';export default function ResetPasswordPage() {const [password, setPassword] = useState('');const [confirmPassword, setConfirmPassword] = useState('');const searchParams = useSearchParams();const token = searchParams.get('token');async function handleSubmit(e) {e.preventDefault();if (password !== confirmPassword) {return setError('密码不匹配');}const response = await fetch('/api/auth/reset-password/confirm', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ token, password })});if (response.ok) {router.push('/login?reset=success');} else {setError('重置密码失败');}}return (<form onSubmit={handleSubmit}><inputtype="password"value={password}onChange={(e) => setPassword(e.target.value)}placeholder="新密码"/><inputtype="password"value={confirmPassword}onChange={(e) => setConfirmPassword(e.target.value)}placeholder="确认密码"/><button type="submit">重置密码</button></form>);
}

总结

Next.js + Prisma + Auth.js 的组合提供了一个强大且灵活的认证方案。关键是:

  • • 正确配置 Auth.js
  • • 实现必要的 API
  • • 做好安全防护
  • • 注意用户体验

掌握这个方案后,可以快速在项目中实现可靠的用户认证系统。

环境配置

创建 .env 文件:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
NEXTAUTH_SECRET="your-secret-key"
NEXTAUTH_URL="http://localhost:3000"

确保将 .env 添加到 .gitignore

.env
.env.local
node_modules

实际应用示例

1. 受保护的仪表板页面

// src/app/dashboard/page.js
import { getServerSession } from "next-auth/next";
import { redirect } from "next/navigation";export default async function DashboardPage() {const session = await getServerSession();if (!session) {redirect("/login");}return (<div><h1>欢迎, {session.user.name}</h1>{/* 仪表板内容 */}</div>);
}

2. 用户状态管理示例

// src/components/UserMenu.js
'use client';
import { useSession } from "next-auth/react";export default function UserMenu() {const { data: session, status } = useSession();if (status === "loading") {return <div>加载中...</div>;}return session ? (<div><span>欢迎, {session.user.name}</span><button onClick={() => signOut()}>退出</button></div>) : (<button onClick={() => signIn()}>登录</button>);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/457439.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探秘 MySQL 数据类型的艺术:性能与存储的精妙平衡

文章目录 前言&#x1f380;一、数据类型分类&#x1f380;二、整数类型&#xff08;举例 TINYINT 和 INT &#xff09;&#x1f3ab;2.1 TINYINT 和 INT 类型的定义2.1.1 TINYINT2.1.2 INT &#x1f3ab;2.2 表的操作示例2.2.1 创建包含 TINYINT 和 INT 类型的表2.2.2 插入数据…

【JavaSE】认识String类,了解,进阶到熟练掌握

#1024程序员节 | 征文# 下面就让博主带领大家一起解决心中关于String类的疑问吧~~~ 1.字符串构造&#xff1a; 第一种和第二种&#xff08;有一定的区别&#xff0c;在常量池上&#xff09; public static void main(String[] args) { // 使用常量串构造 String s1 "h…

最新PHP网盘搜索引擎系统源码 附教程

简介 最新PHP网盘搜索引擎系统源码 附教程 这是一个基于thinkphp5.1MySQL开发的网盘搜索引擎&#xff0c;可以批量导入各大网盘链接&#xff0c;例如百度网盘、阿里云盘、夸克网盘等。 功能特点&#xff1a;网盘失效检测&#xff0c;后台管理功能&#xff0c;网盘链接管理&a…

(三)第一个Qt程序“Qt版本的HelloWorld”

一、随记 我们在学习编程语言的时候&#xff0c;各种讲解编程语言的书籍中通常都会以一个非常经典的“HelloWorld”程序展开详细讲解。程序虽然简短&#xff0c;但是“麻雀虽小&#xff0c;五脏俱全”&#xff0c;但是却非常适合用来熟悉程序结构、规范&#xff0c;快速形成对编…

axure中继器

学习了一点中继器&#xff0c;完成管理后台左侧菜单的功能设置。 样式不太好看&#xff0c;只分享功能&#xff01;这篇写的有点潦草&#xff0c;只供参考。 点击展开隐藏一级菜单 下面是配置交互信息 二级菜单要组合&#xff0c;加载时隐藏&#xff0c;点击一级菜单切换显隐…

在linux系统中查看具体文件大小命令

#!/bin/bash# 检查是否提供了路径 if [ "$#" -ne 1 ]; thenecho "用法: $0 <路径>"exit 1 fiDIRECTORY$1# 检查路径是否存在 if [ ! -d "$DIRECTORY" ]; thenecho "错误: 目录 $DIRECTORY 不存在."exit 1 fi# 定义命令数组 comm…

Linux:定时任务

目录 服务 配置命令 配置格式 定时任务案例 每2分钟同步时间 每天半夜备份文件 服务说明 相关目录&#xff1a; /var/spool/cron/ 用户的定时任务配置文件目录&#xff08;用户制定的任务都在该目录&#xff09; /var/log/cron 定时任务日志 /etc/crontab 系统定时任…

Ajax:请求 响应

Ajax&#xff1a;请求 & 响应 AjaxjQuery的Ajax接口$.get$.post$.ajax PostMan 接口测试getpost Ajax 浏览器中看到的数据&#xff0c;并不是保存在浏览器本地的&#xff0c;而是实时向服务器进行请求的。当服务器接收到请求&#xff0c;就会发回一个响应&#xff0c;此时浏…

基于信号分解和多种深度学习结合的上证指数预测模型

大家好&#xff0c;我是带我去滑雪&#xff01; 为了给投资者提供更准确的投资建议、帮助政府和监管部门更好地制定相关政策&#xff0c;维护市场稳定&#xff0c;本文对股民情绪和上证指数之间的关系进行更深入的研究&#xff0c;并结合信号分解、优化算法和深度学习对上证指数…

TypeScript基础简介

TypeScript是Javascript的一个超集。 TypeScript在原有的基础之上又添加了编译器类型检查的功能&#xff0c;意味着如果使用ts进行开发&#xff0c;会对变量的类型进行较为严格的验证&#xff0c;防止程序员写出可能出错的代码&#xff0c;规范变成习惯&#xff0c;适合大项目开…

ffmpeg视频滤镜:腐蚀滤镜

滤镜简述 erosion 官网链接> FFmpeg Filters Documentation 这个滤镜会在视频上应用腐蚀操作&#xff0c;腐蚀操作是形态学中一种操作&#xff0c;接触过opencv的同学应该很熟悉。滤镜主要有如下作用&#xff1a; 去除噪声&#xff1a;腐蚀可以帮助去除图像中的小颗粒噪…

【Linux学习】(5)软件包管理器yum|编辑器vim

前言 了解Linux的软件生态&#xff0c;学会yum安装软件掌握vim编辑器的使用添加普通用户到系统的信任白名单 一、Linux 软件包管理器 yum 1. Linux安装软件 源代码安装&#xff1a;在Linux中源代码安装软件是一种比较麻烦的方式&#xff0c;他需要你自己配置编译环境、编译源…

Jenkins发布vue项目,版本不一致导致build错误

问题一 yarn.lock文件的存在导致在自动化的时候&#xff0c;频频失败问题二 仓库下载的资源与项目资源版本不一致 本地跑好久的一个项目&#xff0c;现在需要部署在Jenkins上面进行自动化打包部署&#xff1b;想着部署后今后可以省下好多时间&#xff0c;遂兴高采烈地去部署&am…

c++:string类

想要深刻理解string类最好自己实现一个&#xff0c;可以看我的这篇文章&#xff1a;c:模拟实现string类-CSDN博客想要学好库中的string最好自己实现一个出来&#xff0c;能够加深对string的理解。蟹蟹观看&#xff01;关注&#xff01;评论&#xff01;一键三连&#xff01;htt…

模型 支付矩阵

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。策略选择的收益分析工具。 1 支付矩阵的应用 1.1 支付矩阵在市场竞争策略分析中的应用 支付矩阵是一种强大的决策工具&#xff0c;它在多个领域的应用中都发挥着重要作用。以下是一个具体的应用案例…

WebView渲染异常导致闪退解决方案

背景&#xff1a; App主页面使用了大量WebView容器(10个以上)显示图表信息&#xff0c;最新发现bugly上面出现一些关于浏览器Native Crash&#xff0c;如下&#xff1a; 经排查&#xff0c;是WebView渲染失败导致Crash&#xff0c;可以通过webView.loadUrl("chrome://cra…

《Windows PE》7.4 资源表应用

本节我们将通过两个示例程序&#xff0c;演示对PE文件内图标资源的置换与提取。 本节必须掌握的知识点&#xff1a; 更改图标 提取图标资源 7.4.1 更改图标 让我们来做一个实验&#xff0c;替换PE文件中现有的图标。如果手工替换&#xff0c;一定是先找到资源表&#xff0c;…

Linux -- 共享内存(2)

目录 命令 ipcs -m &#xff1a; 命令 ipcrm -m shmid&#xff1a; 共享内存的通信&#xff1a; 为什么共享内存更高效&#xff1f; 代码&#xff1a; ShmClient.cc&#xff1a; ShmServer.cc&#xff1a; 结果&#xff1a; 如何让共享内存实现同步&#xff1f; 代码&a…

基于SSM的BBS社区论坛系统源码

运行环境&#xff1a;ideamysql5.7jdk8maven 使用技术&#xff1a;ssmmysqlshirolayui 功能模块&#xff1a;用户管理、模板管理、帖子管理、公告管理、权限管理等

echarts:导入excel生成桑葚图

前言 前两天帮别人实现了一个小功能&#xff0c;主要是选择excel文件&#xff0c;读取里面的数据&#xff0c;将数据生成桑葚图 echarts官方桑葚图案例 实现 因为就是一个单纯的html文件&#xff0c;用到的库都是通过CDN的方式加载的&#xff0c;会有一些慢 <!DOCTYPE …