技术栈与目标
- Node.js + Express:Web 框架
- mysql2:MySQL 驱动(使用 Promise 版本)
- bcryptjs:密码加盐哈希,不存储明文密码
- jsonwebtoken:签发与校验 JWT
最终实现三个接口:
| 接口 |
方法 |
说明 |
/api/register |
POST |
用户注册,密码加密入库 |
/api/login |
POST |
用户登录,签发 JWT |
/api/user/info |
GET |
携带 Token 获取用户信息(受保护) |
一、项目初始化
1 2 3
| mkdir user-auth && cd user-auth npm init -y npm install express mysql2 bcryptjs jsonwebtoken dotenv
|
目录结构:
1 2 3 4 5
| user-auth/ ├── app.js # 入口 ├── db.js # 数据库连接池 ├── auth.js # 鉴权中间件 └── .env # 环境变量
|
二、数据库与连接池
先建表:
1 2 3 4 5 6 7 8 9 10
| CREATE DATABASE IF NOT EXISTS user_auth DEFAULT CHARSET utf8mb4;
USE user_auth;
CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT 'bcrypt 哈希', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;
|
db.js 使用连接池,避免频繁建立连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const mysql = require('mysql2/promise') require('dotenv').config()
const pool = mysql.createPool({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'user_auth', waitForConnections: true, connectionLimit: 10, charset: 'utf8mb4' })
module.exports = pool
|
三、注册接口:密码必须加盐哈希
明文密码一旦数据库泄露就全部暴露,必须用 bcryptjs 加密(自带盐,同样的密码每次哈希结果都不同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const express = require('express') const bcrypt = require('bcryptjs') const jwt = require('jsonwebtoken') const pool = require('./db')
const app = express() app.use(express.json())
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-me'
app.post('/api/register', async (req, res) => { const { username, password } = req.body
if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }) } if (password.length < 6) { return res.status(400).json({ message: '密码至少 6 位' }) }
const [exist] = await pool.query('SELECT id FROM users WHERE username = ?', [username]) if (exist.length > 0) { return res.status(409).json({ message: '用户名已存在' }) }
const hash = await bcrypt.hash(password, 10)
await pool.query('INSERT INTO users (username, password) VALUES (?, ?)', [username, hash]) res.status(201).json({ message: '注册成功' }) })
|
四、登录接口:校验密码并签发 JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| app.post('/api/login', async (req, res) => { const { username, password } = req.body if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }) }
const [rows] = await pool.query('SELECT * FROM users WHERE username = ?', [username]) if (rows.length === 0) { return res.status(401).json({ message: '用户名或密码错误' }) }
const user = rows[0] const ok = await bcrypt.compare(password, user.password) if (!ok) { return res.status(401).json({ message: '用户名或密码错误' }) }
const token = jwt.sign( { id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '7d' } )
res.json({ token }) })
|
五、鉴权中间件:保护受保护接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const jwt = require('jsonwebtoken')
function auth(req, res, next) { const header = req.headers.authorization if (!header || !header.startsWith('Bearer ')) { return res.status(401).json({ message: '未登录' }) }
const token = header.slice(7) try { req.user = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret-change-me') next() } catch (e) { return res.status(401).json({ message: 'Token 无效或已过期' }) } }
module.exports = auth
|
受保护接口只需要在路由上挂中间件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const auth = require('./auth')
app.get('/api/user/info', auth, async (req, res) => { const [rows] = await pool.query( 'SELECT id, username, created_at FROM users WHERE id = ?', [req.user.id] ) if (rows.length === 0) { return res.status(404).json({ message: '用户不存在' }) } res.json(rows[0]) })
app.listen(3000, () => console.log('Server running at http://localhost:3000'))
|
六、接口测试
用 curl 完整走一遍流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| curl -X POST http://localhost:3000/api/register \ -H "Content-Type: application/json" \ -d '{"username":"zhangsan","password":"123456"}'
curl -X POST http://localhost:3000/api/login \ -H "Content-Type: application/json" \ -d '{"username":"zhangsan","password":"123456"}'
curl http://localhost:3000/api/user/info \ -H "Authorization: Bearer <上一步的token>"
|
安全要点总结
- 密码永远不存明文:用 bcrypt 加盐哈希,成本因子建议 10-12;
- 登录失败统一提示:不要区分”用户不存在”和”密码错误”,防止撞库枚举;
- JWT payload 不放敏感数据:只放
id、username 这类标识信息;
JWT_SECRET 放环境变量:不要写死在代码里,更不要提交到 Git;
- SQL 全部用参数化查询(
? 占位):防止 SQL 注入;
- Token 设置过期时间:
expiresIn: '7d',生产环境还可考虑 refresh token 机制。
总结
到这里,一个可用的注册/登录/JWT 鉴权闭环就完成了。后续可以在这个基础上扩展:refresh token 刷新机制、角色权限(RBAC)、接口限流、统一错误处理中间件等。