前言
JWT(JSON Web Token)是现代 Web 应用中最流行的认证方案之一。本文将使用 Node.js + Express 实现一个完整的 JWT 认证系统,包含注册、登录、Token 刷新和权限控制。
一、项目初始化
mkdir jwt-auth && cd jwt-auth
npm init -y
npm install express jsonwebtoken bcryptjs cors dotenv
npm install -D typescript @types/express @types/node ts-node-dev二、项目结构
src/
├── config/
│ └── index.ts
├── middleware/
│ └── auth.ts
├── routes/
│ └── auth.ts
├── utils/
│ └── jwt.ts
├── types/
│ └── index.ts
└── app.ts三、核心实现
3.1 JWT 工具类
// src/utils/jwt.ts
import jwt from "jsonwebtoken";
import { config } from "../config";
export interface JwtPayload {
userId: string;
role: string;
}
export function signAccessToken(payload: JwtPayload): string {
return jwt.sign(payload, config.JWT_SECRET, {
expiresIn: "15m",
});
}
export function signRefreshToken(payload: JwtPayload): string {
return jwt.sign(payload, config.JWT_REFRESH_SECRET, {
expiresIn: "7d",
});
}
export function verifyAccessToken(token: string): JwtPayload {
return jwt.verify(token, config.JWT_SECRET) as JwtPayload;
}
export function verifyRefreshToken(token: string): JwtPayload {
return jwt.verify(token, config.JWT_REFRESH_SECRET) as JwtPayload;
}3.2 认证中间件
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import { verifyAccessToken } from "../utils/jwt";
// 扩展 Request 类型
declare global {
namespace Express {
interface Request {
user?: { userId: string; role: string };
}
}
}
// 基本认证
export function authRequired(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ code: 401, msg: "未提供认证令牌" });
}
const token = authHeader.substring(7);
try {
const payload = verifyAccessToken(token);
req.user = { userId: payload.userId, role: payload.role };
next();
} catch {
return res.status(401).json({ code: 401, msg: "令牌无效或已过期" });
}
}
// 角色权限控制
export function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ code: 401, msg: "未认证" });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ code: 403, msg: "权限不足" });
}
next();
};
}3.3 路由实现
// src/routes/auth.ts
import { Router, Response } from "express";
import bcrypt from "bcryptjs";
import { authRequired, requireRole } from "../middleware/auth";
import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../utils/jwt";
const router = Router();
// 模拟用户存储(实际应使用数据库)
const users: Map<string, { id: string; username: string; password: string; role: string }> = new Map();
const refreshTokens: Set<string> = new Set();
// 注册
router.post("/register", async (req, res: Response) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ code: 400, msg: "用户名和密码不能为空" });
}
if (users.has(username)) {
return res.status(409).json({ code: 409, msg: "用户已存在" });
}
const hashedPassword = await bcrypt.hash(password, 10);
const id = Date.now().toString();
users.set(username, { id, username, password: hashedPassword, role: "user" });
res.json({ code: 0, msg: "注册成功" });
});
// 登录
router.post("/login", async (req, res: Response) => {
const { username, password } = req.body;
const user = users.get(username);
if (!user) {
return res.status(404).json({ code: 404, msg: "用户不存在" });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ code: 401, msg: "密码错误" });
}
const payload = { userId: user.id, role: user.role };
const accessToken = signAccessToken(payload);
const refreshToken = signRefreshToken(payload);
refreshTokens.add(refreshToken);
res.json({
code: 0,
msg: "登录成功",
data: { accessToken, refreshToken, user: { id: user.id, username: user.username, role: user.role } },
});
});
// 刷新 Token
router.post("/refresh", (req, res: Response) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.has(refreshToken)) {
return res.status(401).json({ code: 401, msg: "无效的刷新令牌" });
}
try {
const payload = verifyRefreshToken(refreshToken);
refreshTokens.delete(refreshToken);
const newPayload = { userId: payload.userId, role: payload.role };
const newAccessToken = signAccessToken(newPayload);
const newRefreshToken = signRefreshToken(newPayload);
refreshTokens.add(newRefreshToken);
res.json({
code: 0,
msg: "刷新成功",
data: { accessToken: newAccessToken, refreshToken: newRefreshToken },
});
} catch {
refreshTokens.delete(refreshToken);
return res.status(401).json({ code: 401, msg: "刷新令牌已过期" });
}
});
// 登出
router.post("/logout", (req, res: Response) => {
const { refreshToken } = req.body;
refreshTokens.delete(refreshToken);
res.json({ code: 0, msg: "已登出" });
});
// 受保护的路由
router.get("/profile", authRequired, (req, res: Response) => {
res.json({ code: 0, data: { userId: req.user!.userId, role: req.user!.role } });
});
// 管理员路由
router.get("/admin/users", authRequired, requireRole("admin"), (req, res: Response) => {
const userList = Array.from(users.values()).map(u => ({
id: u.id, username: u.username, role: u.role,
}));
res.json({ code: 0, data: userList });
});
export default router;3.4 主应用
// src/app.ts
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import authRoutes from "./routes/auth";
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
app.use("/api/auth", authRoutes);
app.get("/", (_req, res) => {
res.json({ msg: "JWT Auth API" });
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});四、安全最佳实践
- Access Token 短时效(15 分钟),Refresh Token 长时效(7 天)
- Refresh Token 一次性使用:每次刷新后旧 Token 失效
- HTTPS 传输:生产环境必须使用 HTTPS
- 密码加密:使用 bcrypt,不要用 MD5/SHA1
- 环境变量管理密钥:不要硬编码到代码中
- Token 黑名单:登出时将 Token 加入黑名单(Redis)
总结
JWT 认证方案无状态、易扩展,适合前后端分离架构。通过 Access Token + Refresh Token 双 Token 机制,可以在安全性和用户体验之间取得平衡。生产环境中建议将 Refresh Token 存储在 Redis 中,方便统一管理和吊销。
评论 (0)