You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:57:15