site logo

Marico's space

使用 Express、Prisma、JWT 和数据库事务构建认证与项目创建

前端技术 2026-06-19 11:28:08 7

认证这事儿,表面上看挺简单。

用户输入邮箱密码,校验一下,登录成功,完事儿。

但等你真正开始做项目了,才发现认证背后牵扯的东西多着呢:安全、授权、数据库设计、Token 管理、中间件、数据一致性……一个都躲不开。

这篇文章聊聊我给一个项目管理应用做的认证系统和项目创建流程,技术栈是:

  • Node.js
  • Express
  • Prisma ORM(对象关系映射)
  • PostgreSQL
  • JWT(JSON Web Token)
  • bcrypt

会覆盖到这些内容:

  1. 密码哈希
  2. JWT 认证
  3. 路由保护中间件
  4. 登录注册接口
  5. Prisma 关联关系
  6. 授权
  7. 数据库事务

开始吧。

理解密码哈希

新手最常问的一个问题:

bcrypt 把密码哈希之后,登录时怎么知道密码对不对?

答案是:

bcrypt 根本不解密任何东西。

哈希是单向操作。

举个例子:

password123

可能变成:

$2a$10$Y7s8J...

这个哈希值没法逆向还原成原始密码。

登录时的流程是这样的:

  1. 用户输入密码
  2. 服务端取出存储的哈希值
  3. bcrypt 对输入的密码做哈希
  4. bcrypt 安全地比对两个值
const match = await bcrypt.compare( password, user.password
)

如果比对成功:

true

用户认证通过。

这样确保密码永远不会以明文形式存储。

构建注册接口

注册流程是这样的:

客户端 ↓
POST /register ↓
校验输入 ↓
检查用户是否已存在 ↓
哈希密码 ↓
创建用户 ↓
生成 JWT ↓
返回响应

第一步是检查邮箱是否已被注册。前端当然会确保表单不为空。

const existingUser = await prisma.user.findUnique({ where: { email } })

如果用户已存在:

return res.status(400).json({ message: "Email already exists"
})

接下来哈希密码。

const hashedPassword = await bcrypt.hash(password, 10)

这个数字 10 代表盐轮数。

值越高越安全,但计算量也越大。

现在可以安全地存储用户了。

const user = await prisma.user.create({ data: { name, email, password: hashedPassword }
})

生成 JWT Token

注册完成后,我们不想让用户再登录一次。

而是直接生成一个 Token。

const token = jwt.sign( { id: user.id }, process.env.JWT_SECRET, { expiresIn: "7d" }
)

Payload 包含:

{ id: user.id
}

然后返回给客户端。

res.status(201).json({ user, token
})

构建登录接口

登录流程差不多。

邮箱
密码 ↓
查找用户 ↓
比对密码 ↓
生成 JWT ↓
返回用户 + Token

先找到用户。

const user = await prisma.user.findUnique({ where: { email } })

永远不要暴露邮箱是否存在这个信息。

错误示范:

Email not found

正确做法:

Invalid credentials

这样能防止攻击者枚举有效账号。

接下来比对密码。

const match = await bcrypt.compare( password, user.password )

比对失败的话:

return res.status(401).json({ message: "Invalid credentials"
})

生成 Token 并返回用户。

返回之前先把密码去掉。

const { password: _, ...safeUser
} = user

这是对象解构。

password 字段被提取出来然后丢弃。

剩下的就成了:

safeUser

现在返回:

{ user: safeUser, token
}

用中间件保护路由

只有认证是不够的。

还需要授权。

举个例子:

GET /projects

只能让已登录用户访问。

这里就要用到中间件。

export function protect( req, res, next
) { const token = req.headers.authorization ?.split(" ")[1] if (!token) { return res.status(401).json({ message: "Not authorized" }) } try { const decoded = jwt.verify( token, process.env.JWT_SECRET ) req.user = decoded next() } catch { return res.status(401).json({ message: "Invalid token" }) }
}

请求头长这样:

Authorization:
Bearer eyJhbG...

中间件提取出 Token。

验证通过的话:

req.user = decoded

这样每个受保护的路由都能访问:

req.user.id

为什么通过 Membership 查询项目

一个用户可以属于多个项目。

一个项目可以包含多个用户。

这是多对多关系。

Schema 示例:

User ↕
ProjectMembership ↕
Project

要查询用户所属的项目:

await prisma.project.findMany({ where: { memberships: { some: { userId: req.user.id } } }
})

关键词:

some

意思是:

至少有一个关联记录匹配。

Prisma 还支持:

every

none

用于更复杂的过滤。

安全地创建项目

创建项目涉及多个数据库写操作。

我们需要:

  1. 创建项目
  2. 创建所有者成员关系
  3. 创建默认看板

没有保护的话,中间某一步可能失败。

想象一下:

项目已创建
成员关系创建失败
看板未创建

现在数据库里就有一个残缺的项目。

解决这个问题,Prisma 提供了事务。

await prisma.$transaction( async (tx) => { ... }
)

事务内部:

const project = await tx.project.create(...)

创建所有者成员关系。

await tx.projectMember.create(...)

创建默认看板。

await tx.board.createMany(...)

最后:

return project

如果任何操作失败:

ROLLBACK

所有操作自动回滚。

这叫原子操作。

要么全部成功,要么全部失败。

为什么事务很重要

很多新手以为事务只在银行系统里有用。

实际上,任何需要多个写操作保持一致性的场景都用得上。

比如:

  • 创建项目
  • 创建订单
  • 处理支付
  • 分配团队成员
  • 预订票务
  • 库存管理

如果业务逻辑需要多个相互依赖的写操作,事务应该是你首先考虑的工具之一。

核心经验总结

做认证系统让我学到了几个重要教训:

  1. 密码要哈希,绝不存明文。
  2. JWT 让用户认证无需存储会话状态。
  3. 中间件让受保护路由保持简洁可复用。
  4. 授权和认证是两码事。
  5. Prisma 关联过滤让查询更强大。
  6. 事务保护数据库一致性。
  7. 安全决策往往比代码本身更重要。

认证通常是开发者搭建的第一个正经后端系统,它引入了软件工程中随处可见的很多概念。

搞懂这些基础,再做更大规模的应用,心里会踏实很多。

好了,就聊到这儿。