
认证这事儿,表面上看挺简单。
用户输入邮箱密码,校验一下,登录成功,完事儿。
但等你真正开始做项目了,才发现认证背后牵扯的东西多着呢:安全、授权、数据库设计、Token 管理、中间件、数据一致性……一个都躲不开。
这篇文章聊聊我给一个项目管理应用做的认证系统和项目创建流程,技术栈是:
会覆盖到这些内容:
开始吧。
新手最常问的一个问题:
bcrypt 把密码哈希之后,登录时怎么知道密码对不对?
答案是:
bcrypt 根本不解密任何东西。
哈希是单向操作。
举个例子:
password123
可能变成:
$2a$10$Y7s8J...
这个哈希值没法逆向还原成原始密码。
登录时的流程是这样的:
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 }
})
注册完成后,我们不想让用户再登录一次。
而是直接生成一个 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
一个用户可以属于多个项目。
一个项目可以包含多个用户。
这是多对多关系。
Schema 示例:
User ↕
ProjectMembership ↕
Project
要查询用户所属的项目:
await prisma.project.findMany({ where: { memberships: { some: { userId: req.user.id } } }
})
关键词:
some
意思是:
至少有一个关联记录匹配。
Prisma 还支持:
every
和
none
用于更复杂的过滤。
创建项目涉及多个数据库写操作。
我们需要:
没有保护的话,中间某一步可能失败。
想象一下:
项目已创建
成员关系创建失败
看板未创建
现在数据库里就有一个残缺的项目。
解决这个问题,Prisma 提供了事务。
await prisma.$transaction( async (tx) => { ... }
)
事务内部:
const project = await tx.project.create(...)
创建所有者成员关系。
await tx.projectMember.create(...)
创建默认看板。
await tx.board.createMany(...)
最后:
return project
如果任何操作失败:
ROLLBACK
所有操作自动回滚。
这叫原子操作。
要么全部成功,要么全部失败。
很多新手以为事务只在银行系统里有用。
实际上,任何需要多个写操作保持一致性的场景都用得上。
比如:
如果业务逻辑需要多个相互依赖的写操作,事务应该是你首先考虑的工具之一。
做认证系统让我学到了几个重要教训:
认证通常是开发者搭建的第一个正经后端系统,它引入了软件工程中随处可见的很多概念。
搞懂这些基础,再做更大规模的应用,心里会踏实很多。
好了,就聊到这儿。