求助:Next.js中Better Auth(MongoDB)登录报错及Expo Router认证方案
解决方案:无Prisma的Next.js Better Auth适配 + Expo Router + Express/MongoDB认证
一、Next.js 端:解决Better Auth "User not found" 错误(无Prisma)
Better Auth默认依赖Prisma作为用户存储适配器,你自行注册的用户不在它默认的查询范围内,必须自定义用户适配器对接现有用户数据库(比如MongoDB)。
1. 核心配置:自定义Better Auth适配器
安装依赖:
npm install better-auth mongodb
创建lib/better-auth.ts,实现Better Auth要求的核心查询方法:
import { BetterAuth } from "better-auth"; import { MongoClient, ObjectId } from "mongodb"; // 连接MongoDB const client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); const db = client.db(); const usersCollection = db.collection("users"); export const auth = BetterAuth({ database: { adapter: { // 登录时根据邮箱查找用户 getUserByEmail: async (email) => { const user = await usersCollection.findOne({ email }); if (!user) return null; // 转换为Better Auth兼容格式 return { id: user._id.toString(), email: user.email, password: user.password, // 确保注册时存储的是哈希后的密码 createdAt: user.createdAt, updatedAt: user.updatedAt, }; }, // 会话验证时根据ID查找用户 getUserById: async (id) => { const user = await usersCollection.findOne({ _id: new ObjectId(id) }); if (!user) return null; return { id: user._id.toString(), email: user.email, password: user.password, createdAt: user.createdAt, updatedAt: user.updatedAt, }; }, // 可选:若需通过Better Auth创建用户,可适配现有逻辑 createUser: async (data) => { const result = await usersCollection.insertOne({ email: data.email, password: data.password, createdAt: new Date(), updatedAt: new Date(), }); return { id: result.insertedId.toString(), ...data, createdAt: new Date(), updatedAt: new Date(), }; }, }, }, emailAndPassword: true, // 启用邮箱密码登录 });
2. 登录、登出与会话管理
登录页面示例:
// app/login/page.tsx "use client"; import { useSignIn, useSession } from "@better-auth/react"; import { useRouter } from "next/navigation"; export default function LoginPage() { const { signIn, error } = useSignIn(); const { session } = useSession(); const router = useRouter(); if (session) { router.push("/"); return null; } const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); await signIn({ email: formData.get("email") as string, password: formData.get("password") as string, }); }; return ( <form onSubmit={handleLogin} className="max-w-md mx-auto mt-10"> {error && <p className="text-red-500 mb-4">{error.message}</p>} <input type="email" name="email" required className="w-full p-2 mb-4 border rounded" placeholder="邮箱" /> <input type="password" name="password" required className="w-full p-2 mb-4 border rounded" placeholder="密码" /> <button type="submit" className="w-full p-2 bg-blue-500 text-white rounded"> 登录 </button> </form> ); }
登出组件示例:
// app/components/LogoutButton.tsx "use client"; import { useSignOut } from "@better-auth/react"; import { useRouter } from "next/navigation"; export default function LogoutButton() { const { signOut } = useSignOut(); const router = useRouter(); const handleLogout = async () => { await signOut(); router.push("/login"); }; return ( <button onClick={handleLogout} className="text-red-500"> 登出 </button> ); }
服务端会话保护:
// app/dashboard/page.tsx import { auth } from "@/lib/better-auth"; import { redirect } from "next/navigation"; export default async function DashboardPage() { const session = await auth.getSession(); if (!session) redirect("/login"); return <div className="mt-10 mx-auto max-w-md">欢迎,{session.user.email}</div>; }
二、Expo Router + Node.js/Express + MongoDB/Mongoose 认证方案
1. 后端(Express + Mongoose)
1.1 用户模型与密码哈希
// backend/models/User.ts import mongoose from "mongoose"; import bcrypt from "bcryptjs"; const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true }, password: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, }); // 保存前自动哈希密码 userSchema.pre("save", async function (next) { if (!this.isModified("password")) return next(); this.password = await bcrypt.hash(this.password, 12); next(); }); // 密码验证方法 userSchema.methods.comparePassword = async function (candidatePassword: string) { return await bcrypt.compare(candidatePassword, this.password); }; export const User = mongoose.model("User", userSchema);
1.2 认证路由与JWT实现
// backend/routes/auth.ts import express from "express"; import jwt from "jsonwebtoken"; import { User } from "../models/User"; const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET!; const JWT_EXPIRES_IN = "7d"; // 登录接口 router.post("/login", async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user) return res.status(404).json({ message: "用户不存在" }); const isPasswordValid = await user.comparePassword(password); if (!isPasswordValid) return res.status(401).json({ message: "密码错误" }); // 生成JWT const token = jwt.sign({ userId: user._id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); res.json({ token, user: { id: user._id, email: user.email } }); } catch (err) { res.status(500).json({ message: "服务器错误" }); } }); // 会话验证中间件 export const protect = async (req: any, res: express.Response, next: express.NextFunction) => { let token; if (req.headers.authorization?.startsWith("Bearer")) { token = req.headers.authorization.split(" ")[1]; } if (!token) return res.status(401).json({ message: "未授权,请登录" }); try { const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; const user = await User.findById(decoded.userId).select("-password"); if (!user) return res.status(404).json({ message: "用户不存在" }); req.user = user; next(); } catch (err) { res.status(401).json({ message: "无效的登录凭证" }); } }; // 示例受保护路由 router.get("/me", protect, (req, res) => { res.json({ user: req.user }); }); export default router;
2. 前端(Expo Router)
2.1 封装Auth工具类
// app/utils/auth.ts import AsyncStorage from "@react-native-async-storage/async-storage"; import axios from "axios"; const API_BASE_URL = "http://你的后端地址/api"; export const login = async (email: string, password: string) => { const response = await axios.post(`${API_BASE_URL}/auth/login`, { email, password }); await AsyncStorage.setItem("authToken", response.data.token); return response.data.user; }; export const logout = async () => { await AsyncStorage.removeItem("authToken"); }; export const getCurrentUser = async () => { const token = await AsyncStorage.getItem("authToken"); if (!token) return null; try { const response = await axios.get(`${API_BASE_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); return response.data.user; } catch (err) { await logout(); return null; } };
2.2 登录页面
// app/login.tsx import { useState } from "react"; import { View, TextInput, Button, Alert, StyleSheet } from "react-native"; import { login } from "../utils/auth"; import { useRouter } from "expo-router"; export default function LoginScreen() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const router = useRouter(); const handleLogin = async () => { try { await login(email, password); router.replace("/"); } catch (err) { Alert.alert("登录失败", (err as Error).message); } }; return ( <View style={styles.container}> <TextInput style={styles.input} placeholder="邮箱" value={email} onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" /> <TextInput style={styles.input} placeholder="密码" value={password} onChangeText={setPassword} secureTextEntry /> <Button title="登录" onPress={handleLogin} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 20, justifyContent: "center", gap: 15, }, input: { borderWidth: 1, borderColor: "#ccc", borderRadius: 8, padding: 12, }, });
2.3 路由保护(Middleware)
// app/_middleware.ts import { NextResponse } from "expo-router"; import { getCurrentUser } from "./utils/auth"; export async function middleware(request: Request) { const user = await getCurrentUser(); const isAuthPage = request.url.includes("/login"); if (!user && !isAuthPage) { return NextResponse.redirect(new URL("/login", request.url)); } if (user && isAuthPage) { return NextResponse.redirect(new URL("/", request.url)); } return NextResponse.next(); } export const config = { matcher: ["/", "/dashboard/:path*"], };
2.4 登出组件
// app/components/LogoutButton.tsx import { Button } from "react-native"; import { logout } from "../utils/auth"; import { useRouter } from "expo-router"; export default function LogoutButton() { const router = useRouter(); const handleLogout = async () => { await logout(); router.replace("/login"); }; return <Button title="登出" onPress={handleLogout} color="red" />; }
内容的提问来源于stack exchange,提问作者Ridwan Ibraheem
相关产品推荐
相关产品推荐

