Bknd密码重置技能Skill bknd-password-reset

这个技能用于在Bknd应用程序中实现密码重置和更改功能。它涵盖服务器端密码更改、构建基于电子邮件的忘记密码流程以及安全考虑。关键词:Bknd、密码重置、后端开发、安全认证、忘记密码流程、服务器端方法、哈希处理、速率限制。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

name: bknd-password-reset description: 在Bknd应用程序中实现密码重置或更改功能时使用。涵盖服务器端密码更改、构建基于电子邮件的忘记密码流程和安全考虑。

密码重置流程

在Bknd应用程序中实现密码重置和更改功能。

先决条件

  • 启用认证的Bknd项目(bknd-setup-auth
  • 配置密码策略
  • 对于基于电子邮件的重置:电子邮件发送能力(外部服务)

重要上下文

Bknd提供:

  • 服务器端changePassword() 方法,用于管理员/系统密码更改
  • 没有内置的忘记密码流程 - 您必须实现令牌生成、电子邮件发送和验证

何时使用UI模式

  • 通过管理面板发起的密码重置

UI步骤: 管理面板 > 数据 > 用户 > 选择用户 > 编辑

注意:通过UI直接编辑密码会设置原始值 - 使用服务器端方法进行适当的哈希处理。

何时使用代码模式

  • 实现基于电子邮件的忘记密码流程
  • 为登录用户添加更改密码功能
  • 管理员工具的密码重置

服务器端密码更改

使用changePassword()方法

import { serve } from "bknd/adapter/node";
import { defineConfig } from "bknd";

export default serve(
  defineConfig({ /* config */ }),
  {
    async seed(ctx) {
      // 通过用户ID更改密码
      await ctx.app.module.auth.changePassword(1, "newSecurePassword123");

      // 或先查找用户
      const { data: user } = await ctx.em.repo("users").findOne({
        email: "user@example.com",
      });
      if (user) {
        await ctx.app.module.auth.changePassword(user.id, "newPassword456");
      }
    },
  }
);

方法签名:

changePassword(userId: number | string, newPassword: string): Promise<boolean>

约束:

  • 用户必须存在
  • 用户必须使用密码策略(非OAuth)
  • 密码使用配置的哈希方法自动哈希处理

构建忘记密码流程

由于Bknd没有内置的忘记密码功能,实现自定义流程:

步骤1:创建重置令牌实体

import { em, entity, text, date, number } from "bknd";

const schema = em({
  password_resets: entity("password_resets", {
    email: text().required(),
    token: text().required().unique(),
    expires_at: date().required(),
    used: number().default(0), // 0 = 未使用, 1 = 已使用
  }),
});

步骤2:请求重置端点

import { randomBytes } from "crypto";

async function requestPasswordReset(email: string, ctx: any) {
  const api = ctx.api;

  // 检查用户是否存在(不要在响应中泄露)
  const { data: user } = await api.data.readOneBy("users", { email });
  if (!user) {
    return { success: true, message: "如果电子邮件存在,重置链接已发送" };
  }

  // 生成安全令牌
  const token = randomBytes(32).toString("hex");
  const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1小时

  // 存储重置令牌
  await api.data.createOne("password_resets", {
    email,
    token,
    expires_at: expiresAt.toISOString(),
    used: 0,
  });

  // 发送电子邮件(使用您的电子邮件服务:SendGrid、Resend等)
  const resetUrl = `https://yourapp.com/reset-password?token=${token}`;
  // await emailService.send({ to: email, subject: "密码重置", ... });

  return { success: true, message: "如果电子邮件存在,重置链接已发送" };
}

步骤3:验证和重置密码

async function resetPassword(token: string, newPassword: string, ctx: any) {
  const api = ctx.api;
  const auth = ctx.app.module.auth;

  // 查找有效令牌
  const { data: resetRecord } = await api.data.readOneBy("password_resets", {
    token,
    used: 0,
  });

  if (!resetRecord) {
    throw new Error("无效或过期的重置令牌");
  }

  // 检查过期时间
  if (new Date(resetRecord.expires_at) < new Date()) {
    throw new Error("重置令牌已过期");
  }

  // 查找用户
  const { data: user } = await api.data.readOneBy("users", {
    email: resetRecord.email,
  });
  if (!user) {
    throw new Error("用户未找到");
  }

  // 更改密码(适当哈希处理)
  await auth.changePassword(user.id, newPassword);

  // 标记令牌为已使用
  await api.data.updateOne("password_resets", resetRecord.id, { used: 1 });

  return { success: true };
}

步骤4:React前端

请求重置表单:

function ForgotPasswordForm() {
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<"idle" | "loading" | "sent">("idle");

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setStatus("loading");

    await fetch("/api/password-reset/request", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email }),
    });
    setStatus("sent");
  }

  if (status === "sent") {
    return <p>检查您的电子邮件获取重置说明。</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="电子邮件"
        required
      />
      <button type="submit" disabled={status === "loading"}>
        {status === "loading" ? "发送中..." : "发送重置链接"}
      </button>
    </form>
  );
}

重置密码表单:

function ResetPasswordForm() {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  const token = searchParams.get("token");

  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [error, setError] = useState("");

  if (!token) return <p>无效的重置链接。</p>;

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (password !== confirmPassword) {
      setError("密码不匹配");
      return;
    }

    const res = await fetch("/api/password-reset/confirm", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token, password }),
    });

    if (res.ok) {
      navigate("/login");
    } else {
      const data = await res.json();
      setError(data.message || "重置失败");
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <p className="error">{error}</p>}
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="新密码"
        minLength={8}
        required
      />
      <input
        type="password"
        value={confirmPassword}
        onChange={(e) => setConfirmPassword(e.target.value)}
        placeholder="确认密码"
        required
      />
      <button type="submit">重置密码</button>
    </form>
  );
}

认证密码更改

对于登录用户更改自己的密码:

async function changePassword(currentPassword: string, newPassword: string) {
  const api = new Api({ host: "http://localhost:7654", storage: localStorage });

  // 通过重新认证验证当前密码
  const { data: userData } = await api.auth.me();
  if (!userData?.user) throw new Error("未认证");

  const { ok } = await api.auth.login("password", {
    email: userData.user.email,
    password: currentPassword,
  });

  if (!ok) throw new Error("当前密码不正确");

  // 调用自定义密码更改端点
  const res = await fetch("/api/password-change", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ newPassword }),
  });

  if (!res.ok) throw new Error("更改密码失败");
}

安全考虑

令牌安全

// 好:密码学安全令牌
import { randomBytes } from "crypto";
const token = randomBytes(32).toString("hex"); // 64字符

// 坏:可预测令牌
const token = Math.random().toString(36); // 不安全!

速率限制

const resetAttempts = new Map<string, { count: number; lastAttempt: Date }>();

function checkRateLimit(email: string): boolean {
  const record = resetAttempts.get(email);
  const now = new Date();

  if (!record || record.lastAttempt < new Date(now.getTime() - 3600000)) {
    resetAttempts.set(email, { count: 1, lastAttempt: now });
    return true;
  }

  if (record.count >= 3) return false; // 每小时最多3次

  record.count++;
  return true;
}

常见陷阱

未哈希密码

// 错误 - 绕过哈希处理
await api.data.updateOne("users", userId, {
  strategy_value: "plainPassword123",
});

// 正确 - 使用配置的哈希处理
await ctx.app.module.auth.changePassword(userId, "plainPassword123");

电子邮件枚举

// 错误 - 泄露电子邮件存在
if (!user) return { error: "电子邮件未找到" };

// 正确 - 不泄露
return { success: true, message: "如果电子邮件存在,重置链接已发送" };

OAuth用户密码重置

const { data: user } = await api.data.readOneBy("users", { email });
if (user?.strategy !== "password") {
  return { error: "此账户使用社交登录" };
}

验证

# 1. 请求重置
curl -X POST http://localhost:7654/api/password-reset/request \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'

# 2. 重置密码(使用来自电子邮件/数据库的令牌)
curl -X POST http://localhost:7654/api/password-reset/confirm \
  -H "Content-Type: application/json" \
  -d '{"token": "<token>", "password": "newPassword123"}'

# 3. 使用新密码登录
curl -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "newPassword123"}'

应该做和不应该做

应该做:

  • 使用changePassword()方法进行适当的哈希处理
  • 生成密码学安全令牌
  • 设置短过期时间(最多1小时)
  • 立即标记令牌为已使用
  • 返回一致的响应(不要泄露电子邮件存在)
  • 速率限制重置请求

不应该做:

  • 存储/传输明文密码
  • 使用可预测令牌(Math.random)
  • 允许无限重置尝试
  • 保持令牌永久有效
  • 允许OAuth用户的密码重置

相关技能

  • bknd-setup-auth - 配置认证系统
  • bknd-login-flow - 登录/注销功能
  • bknd-registration - 用户注册设置
  • bknd-session-handling - 管理用户会话
  • bknd-custom-endpoint - 创建自定义API端点