Skip to content
认证示例
概述
本例基于 Express 与 jsonwebtoken 实现用户注册、登录、JWT 签发与验证、接口权限控制。密码存储使用 bcryptjs 做哈希。示例以内存存储模拟用户数据,重点展示认证链路的串联方式。
基本概念
JWT(JSON Web Token) 是一种紧凑、URL 安全的令牌格式,用于在客户端与服务端之间传递声明。令牌由三部分组成:头部(Header)、负载(Payload)、签名(Signature)。签名保证令牌在传输过程中未被篡改,接收方可验证签发者身份。
认证(Authentication) 指确认用户身份的过程,登录成功后服务端签发 JWT,客户端在后续请求中携带该令牌。
授权(Authorization) 指判断已认证用户拥有的操作权限,通常基于令牌中携带的角色或权限信息进行控制。
工作原理
- 客户端发送注册请求,服务端将用户名与哈希后的密码存储至数据库(本例为内存存储)。
- 客户端发送登录请求,服务端校验用户名与密码,成功后生成带过期时间的 JWT,其中包含用户标识与角色,返回给客户端。
- 客户端在后续请求的
Authorization头中携带该令牌(Bearer <token>)。 - 服务端通过认证中间件在请求到达业务处理逻辑之前解析并验证令牌。如果令牌有效,将解析出的用户信息写入请求上下文。
- 业务处理器从请求上下文获取当前用户信息;需要特定角色的接口,由额外的授权中间件做校验。
- 令牌无效、过期或缺失时,中间件直接返回 401 或 403,不会进入后续处理。
基本用法
安装依赖
bash
npm install express jsonwebtoken bcryptjs目录结构
project/
├── server.js
├── utils/
│ ├── jwt.js
│ └── hash.js
├── middleware/
│ └── auth.js
├── routes/
│ ├── auth.js
│ └── admin.js
└── data/
└── users.js内存存储
data/users.js 提供基于数组的简易用户存储,用于演示。
js
// data/users.js
const users = [];
module.exports = {
findByUsername(username) {
return users.find(u => u.username === username);
},
save(user) {
users.push(user);
}
};实际环境中需替换为持久化数据库。
JWT 工具模块
utils/jwt.js 封装令牌的签发与验证。
js
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET || 'change-me';
function generateToken(username, roles) {
return jwt.sign({ username, roles }, SECRET, { expiresIn: '2h' });
}
function verifyToken(token) {
return jwt.verify(token, SECRET);
}
module.exports = { generateToken, verifyToken };密码哈希
utils/hash.js 提供密码哈希与比对。
js
const bcrypt = require('bcryptjs');
async function hashPassword(plain) {
return bcrypt.hash(plain, 10);
}
async function comparePassword(plain, hash) {
return bcrypt.compare(plain, hash);
}
module.exports = { hashPassword, comparePassword };路由实现
routes/auth.js 包含注册、登录、当前用户三个端点。
js
// routes/auth.js
const express = require('express');
const router = express.Router();
const users = require('../data/users');
const { hashPassword, comparePassword } = require('../utils/hash');
const { generateToken } = require('../utils/jwt');
const { authenticate } = require('../middleware/auth');
router.post('/register', async (req, res) => {
const { username, password } = req.body;
const existing = users.findByUsername(username);
if (existing) {
return res.status(409).json({ message: '用户已存在' });
}
const hashed = await hashPassword(password);
users.save({ username, password: hashed });
res.status(201).json({ message: '注册成功' });
});
router.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.findByUsername(username);
if (!user || !(await comparePassword(password, user.password))) {
return res.status(401).json({ message: '用户名或密码错误' });
}
const token = generateToken(user.username, user.roles || ['ROLE_USER']);
res.json({ accessToken: token });
});
router.get('/me', authenticate, (req, res) => {
res.json({
name: req.user.username,
authorities: req.user.roles
});
});
module.exports = router;注册
向 POST /api/auth/register 发送请求,请求体为 {"username":"alice","password":"secret"},用户名不重复时返回 201 及注册成功消息;重复则返回 409。
登录
向 POST /api/auth/login 发送凭据,验证通过后返回 JSON,其中 accessToken 即为 JWT。客户端应保存该令牌,并在后续请求中通过 Authorization: Bearer <accessToken> 携带。
当前用户
认证通过后访问 GET /api/auth/me,直接返回 JWT 中存放的用户名与角色,不额外查询数据库。响应示例:
json
{
"name": "alice",
"authorities": ["ROLE_USER"]
}认证中间件
middleware/auth.js 解析并验证请求头中的令牌。
js
const { verifyToken } = require('../utils/jwt');
function authenticate(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ message: '未提供令牌' });
}
const token = header.split(' ')[1];
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ message: '令牌无效或已过期' });
}
}
module.exports = { authenticate };管理员接口与角色控制
routes/admin.js 演示需要 ROLE_ADMIN 角色的接口。
js
// routes/admin.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
function requireRole(role) {
return (req, res, next) => {
if (!req.user || !req.user.roles.includes(role)) {
return res.status(403).json({ message: '无权限' });
}
next();
};
}
router.delete('/users/:id', authenticate, requireRole('ROLE_ADMIN'), (req, res) => {
// 执行删除逻辑
res.json({ message: '已删除' });
});
module.exports = router;缺少所需角色时返回 403。
组装应用
js
const express = require('express');
const app = express();
app.use(express.json());
const authRoutes = require('./routes/auth');
const adminRoutes = require('./routes/admin');
app.use('/api/auth', authRoutes);
app.use('/api/admin', adminRoutes);
app.listen(3000, () => console.log('启动在 3000 端口'));示例
一个完整的请求流:
bash
# 注册
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}'
# 响应: {"message":"注册成功"}
# 登录
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}'
# 响应: {"accessToken":"eyJhbGciOi..."}
# 访问受保护资源
curl http://localhost:3000/api/auth/me \
-H "Authorization: Bearer eyJhbGciOi..."
# 响应: {"name":"alice","authorities":["ROLE_USER"]}
# 以普通用户访问管理员接口
curl -X DELETE http://localhost:3000/api/admin/users/1 \
-H "Authorization: Bearer eyJhbGciOi..."
# 响应: 403 {"message":"无权限"}注意点
- 注册时必须对密码做哈希处理,否则登录时
compare操作会因为存储的是明文或格式不一致而失败。 - JWT 验证中间件需要在所有需认证的路由之前加载。如果执行顺序靠后,请求可能在到达该中间件之前被放行,导致未认证访问。
- 路由规划中需明确无需认证的路径(如
/register、/login),避免将敏感接口错误暴露为公开端点。 - 使用 JWT 的系统中,服务端不应再启用传统的 Session 存储。两者混用且策略不清晰时,可能出现令牌有效但会话已销毁(或反之)的状态不一致。
- 签名密钥不应硬编码在源码中,通过环境变量注入,并在实际运行中定期轮换。
限制
- 令牌撤销困难:JWT 无状态,签发后服务端无法主动使其失效。若需实现强制下线或实时撤销某令牌,必须引入黑名单或版本号等机制,这会增加维护成本并部分抵消无状态的优势。
- 会话管理能力受限:由于令牌存在于客户端且无服务端会话对应,像多地登录互踢、在线用户管理等功能需要自行设计额外机制。
- 负载不宜存放敏感信息:JWT 的 Payload 仅经过 Base64 编码,并非加密保护。除非结合 JWE 进行加密,否则不应在里面放入密码、身份证号等敏感数据。
- 令牌体积约束:HTTP 请求头通常有 8KB 左右的限制,过大的载荷会导致请求被服务器或中间件拒绝。
应用
该认证模式适用于前后端分离的 Web 应用,尤其是 SPA 或其他客户端通过 API 访问后端的场景。JWT 的无状态特性有利于横向扩展,角色授权可灵活结合现有权限模型。
