Node.js JWT 认证完整实现:注册登录到权限控制

Node.js JWT 认证完整实现:注册登录到权限控制

admin
2026-07-23 / 0 评论 / 1 阅读

前言

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");
});

四、安全最佳实践

  1. Access Token 短时效(15 分钟),Refresh Token 长时效(7 天)
  2. Refresh Token 一次性使用:每次刷新后旧 Token 失效
  3. HTTPS 传输:生产环境必须使用 HTTPS
  4. 密码加密:使用 bcrypt,不要用 MD5/SHA1
  5. 环境变量管理密钥:不要硬编码到代码中
  6. Token 黑名单:登出时将 Token 加入黑名单(Redis)

总结

JWT 认证方案无状态、易扩展,适合前后端分离架构。通过 Access Token + Refresh Token 双 Token 机制,可以在安全性和用户体验之间取得平衡。生产环境中建议将 Refresh Token 存储在 Redis 中,方便统一管理和吊销。

0

评论 (0)

取消
0:00