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

如何为Prisma ORM编写查询解密钩子以实现加密邮箱登录?

解决加密邮箱无法用于Prisma登录查询的问题

你的核心问题是AES加密因随机IV导致同一明文生成不同密文,无法直接用明文邮箱匹配数据库中的加密字段。以下是三种可行解决方案,按推荐优先级排序:

方案一:改用确定性AES加密(推荐)

通过固定IV(或与明文绑定的IV)让同一明文生成相同密文,直接用加密后的明文作为查询条件,兼顾性能与安全性。

实现步骤

  1. 修改加密/解密函数,使用固定IV:
const CryptoJS = require('crypto-js');

// 从环境变量读取密钥和固定IV(AES-256需要32位密钥,IV为16位)
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;
const FIXED_IV = CryptoJS.enc.Hex.parse(process.env.FIXED_IV);

// 加密函数
function encrypt(text) {
  return CryptoJS.AES.encrypt(text, ENCRYPTION_KEY, { iv: FIXED_IV }).toString();
}

// 解密函数
function decrypt(ciphertext) {
  const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY, { iv: FIXED_IV });
  return bytes.toString(CryptoJS.enc.Utf8);
}
  1. 注册用户时,用上述encrypt函数处理邮箱后存入数据库。
  2. 修改登录端点逻辑,先加密明文邮箱再查询:
fastify.post(
  "/login",
  { /* schema 保持不变 */ },
  async (request, reply) => {
    const { email, password } = request.body;
    const encryptedEmail = encrypt(email); // 先加密明文邮箱

    const prisma = new PrismaClient();
    try {
      const user = await prisma.users.findFirst({
        where: { email: encryptedEmail }, // 用加密后的邮箱查询
      });

      if (!user) {
        reply.code(204);
        return;
      }

      const match = await fastify.bcrypt.compare(password, user.password);
      if (!match) {
        reply.code(204);
        return;
      }

      // 注意:不要将密码存入JWT,仅保留必要的用户标识
      const decryptedEmail = decrypt(user.email);
      const token = fastify.jwt.sign(
        { email: decryptedEmail },
        { expiresIn: "12h" }
      );

      reply.code(200);
      return { token };
    } finally {
      prisma.$disconnect();
    }
  }
);

安全提示:固定IV会让相同明文生成相同密文,若密钥泄露存在一定风险。可进一步优化为给每个用户生成唯一盐(与密文一起存储),用盐作为IV,但会增加查询复杂度,需根据业务安全需求权衡。

方案二:Postgres数据库层面解密查询

利用Postgres的pgcrypto扩展创建解密函数,直接在数据库层面解密字段并匹配明文,无需修改加密逻辑。

实现步骤

  1. 启用Postgres的pgcrypto扩展:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
  1. 创建解密函数(需与代码中加密算法、密钥、IV一致):
CREATE OR REPLACE FUNCTION decrypt_email(ciphertext text)
RETURNS text AS $$
BEGIN
  -- 替换为你的加密密钥和IV,算法需与代码中匹配(示例为AES-256-CBC)
  RETURN pgp_sym_decrypt(ciphertext::bytea, '你的加密密钥', 'cipher-algo=aes256, iv=你的固定IV');
END;
$$ LANGUAGE plpgsql;
  1. 修改登录端点,使用Prisma原生查询调用解密函数:
fastify.post(
  "/login",
  { /* schema 保持不变 */ },
  async (request, reply) => {
    const { email, password } = request.body;

    const prisma = new PrismaClient();
    try {
      // 调用数据库解密函数匹配明文邮箱
      const users = await prisma.$queryRaw`
        SELECT * FROM users WHERE decrypt_email(email) = ${email}
      `;

      const user = users[0];
      if (!user) {
        reply.code(204);
        return;
      }

      const match = await fastify.bcrypt.compare(password, user.password);
      if (!match) {
        reply.code(204);
        return;
      }

      const token = fastify.jwt.sign(
        { email: email },
        { expiresIn: "12h" }
      );

      reply.code(200);
      return { token };
    } finally {
      prisma.$disconnect();
    }
  }
);

性能提示:该方案会对所有用户的邮箱字段执行解密操作,用户量较大时性能会下降,适合小型应用。

方案三:Prisma中间件实现查询后匹配

通过Prisma中间件自动解密查询结果中的邮箱字段,再在代码中匹配明文。此方案性能最差,仅适合用户量极小的场景。

实现步骤

  1. 注册Prisma中间件:
const prisma = new PrismaClient();

prisma.$use(async (params, next) => {
  const result = await next(params);

  // 仅对用户表的查询操作解密邮箱
  if (params.model === 'users' && ['findFirst', 'findMany'].includes(params.action)) {
    const decryptUserEmail = (user) => ({
      ...user,
      email: decrypt(user.email) // 用你的解密函数
    });

    return Array.isArray(result) ? result.map(decryptUserEmail) : decryptUserEmail(result);
  }

  return result;
});
  1. 修改登录端点,查询所有用户后匹配明文邮箱:
fastify.post(
  "/login",
  { /* schema 保持不变 */ },
  async (request, reply) => {
    const { email, password } = request.body;

    const prisma = new PrismaClient();
    try {
      // 查询所有用户(用户量大会严重拖慢性能)
      const users = await prisma.users.findMany();
      const user = users.find(u => u.email === email);

      if (!user) {
        reply.code(204);
        return;
      }

      const match = await fastify.bcrypt.compare(password, user.password);
      if (!match) {
        reply.code(204);
        return;
      }

      const token = fastify.jwt.sign(
        { email: user.email },
        { expiresIn: "12h" }
      );

      reply.code(200);
      return { token };
    } finally {
      prisma.$disconnect();
    }
  }
);

内容的提问来源于stack exchange,提问作者Nickolas de Luca Alberton

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 00:18:22