4、用户注册与登录模块(后端):用户模型完善、密码加密(bcrypt)、JWT令牌生成与验证、注册接口开发、登录接口开发
好,咱们进入实战环节的第一个硬核模块——用户注册与登录。
说实话,这个模块我做了不下几十次了。从最早用MD5存密码,到后来加盐,再到用bcrypt,踩过的坑能写一本小册子。今天咱们就一步到位,把用户模型、密码加密、JWT令牌、注册和登录接口全部搞定。
4.1 用户模型完善
之前我们搭了个基础架子,现在要把用户模型真正用起来。先看看需要哪些字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键,自增 |
| username | STRING(50) | 用户名,唯一 |
| STRING(100) | 邮箱,唯一 | |
| password | STRING(255) | 加密后的密码 |
| role | ENUM('admin','user') | 角色,默认user |
| status | INTEGER | 状态:1启用,0禁用 |
| createdAt | DATE | 创建时间 |
| updatedAt | DATE | 更新时间 |
嗯,这里要注意一点:密码字段长度一定要给够。bcrypt生成的哈希值通常是60个字符左右,但不同轮数可能更长。我建议直接给255,省得以后出问题。
在Sequelize里,模型定义是这样的:
// models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
validate: {
len: [2, 50]
}
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false
},
role: {
type: DataTypes.ENUM('admin', 'user'),
defaultValue: 'user'
},
status: {
type: DataTypes.INTEGER,
defaultValue: 1
}
}, {
timestamps: true
});
module.exports = User;
4.2 密码加密(bcrypt)
密码不能明文存,这是底线。为什么用bcrypt?说白了,它自带盐值,而且计算速度可以调节。MD5那种秒出的算法,在GPU面前跟纸糊的一样。
安装依赖:
npm install bcryptjs
注意我用的是bcryptjs,纯JavaScript实现,不需要编译。如果你用bcrypt,需要装C++编译工具,在Windows上经常出问题。我个人建议直接用bcryptjs,省心。
封装一个密码工具类:
// utils/password.js
const bcrypt = require('bcryptjs');
const SALT_ROUNDS = 10;
// 加密密码
async function hashPassword(password) {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
return bcrypt.hash(password, salt);
}
// 验证密码
async function comparePassword(password, hash) {
return bcrypt.compare(password, hash);
}
module.exports = { hashPassword, comparePassword };
4.3 JWT令牌生成与验证
用户登录后,怎么证明「他是他」?传统方案用session,但分布式部署时很麻烦。JWT(JSON Web Token)就解决了这个问题——令牌自带用户信息,服务端不需要存状态。
安装依赖:
npm install jsonwebtoken
JWT工具类:
// utils/jwt.js
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const EXPIRES_IN = '7d';
// 生成令牌
function generateToken(payload) {
return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN });
}
// 验证令牌
function verifyToken(token) {
try {
return jwt.verify(token, SECRET_KEY);
} catch (error) {
return null;
}
}
module.exports = { generateToken, verifyToken };
JWT的结构分三部分:Header、Payload、Signature。说白了就是:
- Header:声明算法类型,比如HS256
- Payload:存放用户信息,比如userId、role
- Signature:用密钥对前两部分签名,防止篡改
你想想看,如果中间人改了Payload里的userId,Signature验证就会失败。这就是JWT的安全基础。
4.4 注册接口开发
注册接口的逻辑很清晰:接收用户名、邮箱、密码 → 验证是否已存在 → 加密密码 → 存入数据库 → 返回成功。
但这里有个细节:密码不要返回给前端。哪怕加密了也不行,这是安全规范。
// routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { hashPassword } = require('../utils/password');
// 注册接口
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// 检查用户名是否已存在
const existingUser = await User.findOne({
where: {
[Op.or]: [{ username }, { email }]
}
});
if (existingUser) {
return res.status(400).json({
code: 400,
message: '用户名或邮箱已存在'
});
}
// 加密密码
const hashedPassword = await hashPassword(password);
// 创建用户
const user = await User.create({
username,
email,
password: hashedPassword
});
// 返回用户信息(不包含密码)
res.status(201).json({
code: 201,
message: '注册成功',
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
});
user.toJSON(),结果密码字段也暴露了。后来我学乖了,要么用attributes排除,要么手动构造返回对象。
4.5 登录接口开发
登录接口比注册多一步:验证密码 + 生成JWT。
// 登录接口
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// 查找用户
const user = await User.findOne({
where: { username }
});
if (!user) {
return res.status(401).json({
code: 401,
message: '用户名或密码错误'
});
}
// 检查用户状态
if (user.status === 0) {
return res.status(403).json({
code: 403,
message: '账号已被禁用'
});
}
// 验证密码
const isMatch = await comparePassword(password, user.password);
if (!isMatch) {
return res.status(401).json({
code: 401,
message: '用户名或密码错误'
});
}
// 生成JWT
const token = generateToken({
userId: user.id,
username: user.username,
role: user.role
});
res.json({
code: 200,
message: '登录成功',
data: {
token,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
}
});
} catch (error) {
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
});
为什么登录成功要返回token?因为后续所有需要鉴权的接口,前端都要在请求头里带上这个token。格式是:Authorization: Bearer <token>。
- 登录失败不要告诉用户是「用户名不存在」还是「密码错误」,统一返回「用户名或密码错误」
- JWT过期时间不要太长,7天是比较合理的值
- 生产环境一定要用HTTPS,否则token会被中间人截获
4.6 完整流程测试
接口写完了,咱们用Postman测试一下:
- 注册:POST /api/auth/register,body传username、email、password
- 登录:POST /api/auth/login,body传username、password
- 验证:登录成功后拿到token,在后续请求的Header里加Authorization
嗯,到这里用户注册登录模块就完成了。下一章咱们会写一个JWT中间件,用来保护需要登录才能访问的接口。到时候你就知道token怎么用了。
记住一句话:用户认证是系统的第一道门,门没锁好,里面装修得再漂亮也没用。