4、用户注册与登录:JWT令牌生成与验证、注册接口开发、登录接口开发、密码加密存储
用户注册和登录,这是每个OA系统的「大门」。
我见过太多项目,前期图省事,密码明文存、令牌随便发,结果上线没几天就被脱裤了。嗯,咱们今天就把这扇门焊死,从密码加密到JWT令牌,一步步搭起来。
4.1 密码加密存储:别拿用户的安全开玩笑
先说个我踩过的坑。几年前我做一个内部系统,图方便用了MD5加密密码。结果有一次数据库被拖库,虽然只是测试环境,但想想都后怕。从那以后,我再也不敢用MD5、SHA1这类「快哈希」算法了。
为什么?因为这些算法设计出来就是为了快。你想想看,黑客每秒能算几十亿次MD5,你的密码再复杂也扛不住彩虹表攻击。
正确的做法是用 bcrypt 或 Argon2。它们自带盐值,而且计算速度可以调节——慢到让黑客崩溃,但对用户来说只是多等几百毫秒。
核心原则:永远不要自己写加密算法。用业界公认的库,比如 Node.js 的 bcrypt 包。
安装很简单:
npm install bcrypt
注册时加密密码:
const bcrypt = require('bcrypt');
const saltRounds = 10;
async function hashPassword(plainPassword) {
const salt = await bcrypt.genSalt(saltRounds);
const hash = await bcrypt.hash(plainPassword, salt);
return hash;
}
登录时验证密码:
async function verifyPassword(plainPassword, hash) {
const match = await bcrypt.compare(plainPassword, hash);
return match; // true 或 false
}
小提示:saltRounds 设为 10 是平衡点。太低不安全,太高用户登录要等好几秒。我一般用 10-12 之间。
4.2 注册接口开发:把好第一道关
注册接口说白了就是接收用户信息,校验合法性,然后存到数据库。但这里有几个细节要注意。
我个人习惯的注册流程:
- 参数校验:用户名、密码、邮箱等不能为空,密码长度至少8位,邮箱格式要合法。
- 唯一性检查:用户名和邮箱不能重复。我遇到过有人用同一个邮箱注册了十几个账号,搞得系统里全是垃圾数据。
- 密码加密:用上面说的 bcrypt 处理。
- 存入数据库:只存加密后的密码,永远不存明文。
- 返回结果:注册成功返回用户基本信息,但不要返回密码字段。
代码示例:
// 注册接口
app.post('/api/register', async (req, res) => {
try {
const { username, password, email } = req.body;
// 1. 参数校验
if (!username || !password || !email) {
return res.status(400).json({ message: '所有字段都是必填的' });
}
if (password.length < 8) {
return res.status(400).json({ message: '密码至少8位' });
}
// 2. 唯一性检查
const existingUser = await User.findOne({
$or: [{ username }, { email }]
});
if (existingUser) {
return res.status(409).json({ message: '用户名或邮箱已存在' });
}
// 3. 密码加密
const hashedPassword = await hashPassword(password);
// 4. 存入数据库
const newUser = new User({
username,
password: hashedPassword,
email
});
await newUser.save();
// 5. 返回结果(不返回密码)
res.status(201).json({
message: '注册成功',
user: {
id: newUser._id,
username: newUser.username,
email: newUser.email
}
});
} catch (error) {
res.status(500).json({ message: '服务器内部错误' });
}
});
注意:千万不要在返回结果里包含密码字段,哪怕是加密后的也不行。我曾经见过一个项目,返回用户信息时把 hash 也带上了,虽然比明文好一点,但依然是不安全的做法。
4.3 JWT令牌生成与验证:无状态认证的核心
JWT(JSON Web Token)说白了就是一个「通行证」。用户登录成功后,服务器发给他一个令牌,以后每次请求都带上这个令牌,服务器就知道你是谁了。
为什么用JWT?因为它是无状态的。服务器不需要存session,扩展起来特别方便。我做过一个项目,用户量从几千涨到几十万,认证系统几乎没改过,这就是JWT的好处。
JWT的结构分三部分:
| 部分 | 说明 | 示例 |
|---|---|---|
| Header | 声明类型和加密算法 | {"alg": "HS256", "typ": "JWT"} |
| Payload | 存放用户信息,比如用户ID、角色 | {"userId": "123", "role": "admin"} |
| Signature | 对前两部分签名,防止篡改 | 用密钥加密后的字符串 |
安装 jsonwebtoken 包:
npm install jsonwebtoken
生成令牌:
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
function generateToken(user) {
const payload = {
userId: user._id,
username: user.username,
role: user.role || 'user'
};
// 设置过期时间,我一般用 7 天
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
return token;
}
验证令牌:
function verifyToken(token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
return decoded; // 返回 payload 中的信息
} catch (error) {
// 令牌过期或无效
return null;
}
}
经验之谈:JWT_SECRET 一定要放在环境变量里,不要硬编码在代码中。我习惯用 crypto.randomBytes(64).toString('hex') 生成一个足够长的密钥。
4.4 登录接口开发:验证身份,发放令牌
登录接口的逻辑其实很简单:
- 接收用户名和密码
- 根据用户名查找用户
- 用 bcrypt 验证密码
- 验证通过则生成JWT令牌
- 返回令牌和用户信息
代码实现:
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
// 1. 参数校验
if (!username || !password) {
return res.status(400).json({ message: '用户名和密码不能为空' });
}
// 2. 查找用户
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 3. 验证密码
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 4. 生成令牌
const token = generateToken(user);
// 5. 返回结果
res.json({
message: '登录成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({ message: '服务器内部错误' });
}
});
安全提示:登录失败时,不要告诉用户「用户名不存在」或「密码错误」这种具体信息。统一返回「用户名或密码错误」,防止黑客通过枚举用户名来试探有效账号。
4.5 中间件:保护需要登录的接口
有了JWT令牌,我们还需要一个中间件来保护那些需要登录才能访问的接口。比如查看个人资料、提交审批等。
// 认证中间件
function authMiddleware(req, res, next) {
// 从请求头中获取令牌
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: '未提供认证令牌' });
}
const token = authHeader.split(' ')[1];
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({ message: '令牌无效或已过期' });
}
// 将用户信息挂载到请求对象上
req.user = decoded;
next();
}
// 使用中间件保护接口
app.get('/api/profile', authMiddleware, async (req, res) => {
const user = await User.findById(req.user.userId);
res.json({
id: user._id,
username: user.username,
email: user.email,
role: user.role
});
});
注意:前端在每次请求时,需要在请求头中带上 Authorization: Bearer <token>。如果令牌过期了,前端需要跳转到登录页让用户重新登录。
4.6 完整流程总结
咱们把整个流程串起来看看:
- 注册:用户提交信息 → 后端校验 → bcrypt加密密码 → 存入数据库 → 返回成功
- 登录:用户提交凭证 → 后端查找用户 → bcrypt验证密码 → 生成JWT令牌 → 返回令牌
- 访问受保护接口:前端携带令牌 → 中间件验证令牌 → 通过则放行 → 返回数据
这套流程我用了好几年,从几十个用户的小系统到几万人的企业OA,从来没出过安全问题。说白了,只要把密码加密和令牌验证这两块做扎实了,认证这块基本就稳了。
下一章咱们聊聊权限控制——有了用户身份之后,怎么控制谁能看什么、谁能做什么。嗯,那才是OA系统的精髓所在。