如何为Prisma ORM编写查询解密钩子以实现加密邮箱登录?
解决加密邮箱无法用于Prisma登录查询的问题
你的核心问题是AES加密因随机IV导致同一明文生成不同密文,无法直接用明文邮箱匹配数据库中的加密字段。以下是三种可行解决方案,按推荐优先级排序:
方案一:改用确定性AES加密(推荐)
通过固定IV(或与明文绑定的IV)让同一明文生成相同密文,直接用加密后的明文作为查询条件,兼顾性能与安全性。
实现步骤
- 修改加密/解密函数,使用固定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); }
- 注册用户时,用上述
encrypt函数处理邮箱后存入数据库。 - 修改登录端点逻辑,先加密明文邮箱再查询:
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扩展创建解密函数,直接在数据库层面解密字段并匹配明文,无需修改加密逻辑。
实现步骤
- 启用Postgres的
pgcrypto扩展:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
- 创建解密函数(需与代码中加密算法、密钥、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;
- 修改登录端点,使用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中间件自动解密查询结果中的邮箱字段,再在代码中匹配明文。此方案性能最差,仅适合用户量极小的场景。
实现步骤
- 注册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; });
- 修改登录端点,查询所有用户后匹配明文邮箱:
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
相关产品推荐
相关产品推荐

