认证(Authentication)解决「你是谁」,授权(Authorization/权限)解决「你能做什么」。本专题覆盖 Node 服务端主流的 Session + Cookie 与 JWT 两种认证方式,以及 Passport 生态和 RBAC 权限控制,最后给出安全加固清单。
前置知识:建议先阅读 Express 基础 与 中间件。
| 维度 | Session + Cookie | JWT |
|---|---|---|
| 状态存储 | 服务器内存/Redis | 客户端持有,无状态 |
| 扩展性 | 需共享 Session 存储 | 天然支持水平扩展 |
| 失效控制 | 可即时踢除 | 登出/失效较难(需黑名单) |
| 安全风险 | CSRF 需关注 | XSS 窃取 token 需关注 |
| 适用场景 | 传统 Web 网站 | 前后端分离 API / 移动端 |
npm install express-sessionconst express = require("express");
const session = require("express-session");
const app = express();
app.use(express.json());
app.use(session({
secret: "your-secret-key", // 会话签名密钥
resave: false, // 会话不变时是否强制保存
saveUninitialized: false, // 是否保存未初始化会话
cookie: {
httpOnly: true, // 禁止 JS 读取,防 XSS 窃取
maxAge: 1000 * 60 * 60, // 1 小时
secure: process.env.NODE_ENV === "production", // 生产走 HTTPS
sameSite: "lax", // 缓解 CSRF
},
}));
// 登录:写入会话
app.post("/login", (req, res) => {
const { username, password } = req.body;
if (username === "admin" && password === "123456") {
req.session.user = { id: 1, username }; // 关键:保存登录态
return res.json({ message: "登录成功" });
}
res.status(401).json({ error: "用户名或密码错误" });
});
// 受保护:读取会话
app.get("/profile", (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: "请先登录" });
}
res.json({ user: req.session.user });
});
// 登出:销毁会话
app.post("/logout", (req, res) => {
req.session.destroy(() => {
res.json({ message: "已登出" });
});
});多实例部署时不共享内存会话,需存到 Redis:
npm install connect-redis redisconst { createClient } = require("redis");
const { RedisStore } = require("connect-redis");
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();
app.use(session({
// 会话过期时间跟随 cookie.maxAge(connect-redis v6+ 已移除 ttl 选项)
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, maxAge: 3600000 },
}));npm install jsonwebtokenconst jwt = require("jsonwebtoken");
const SECRET = process.env.JWT_SECRET;
// 登录签发
app.post("/login", (req, res) => {
const { username, password } = req.body;
if (username !== "admin" || password !== "123456") {
return res.status(401).json({ error: "用户名或密码错误" });
}
const token = jwt.sign(
{ userId: 1, username },
SECRET,
{ expiresIn: "1d" }
);
res.json({ token });
});
// 认证中间件
const authMiddleware = (req, res, next) => {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: "缺少 token" });
try {
req.user = jwt.verify(token, SECRET);
next();
} catch {
res.status(401).json({ error: "token 无效或已过期" });
}
};
// 受保护接口
app.get("/profile", authMiddleware, (req, res) => {
res.json({ user: req.user });
});Header.Payload.Signature{ alg, typ },哈希算法。{ userId, username, iat, exp }(不存敏感信息,仅 base64 可解)。HMAC-SHA256(Header.Payload, secret),用于防篡改。express-jwt 简化npm install express-jwtconst { expressjwt } = require("express-jwt");
app.use(expressjwt({ secret: SECRET, algorithms: ["HS256"] })
.unless({ path: ["/login", "/register"] }));
// 之后每个请求 req.auth 即为解码后的 payloadrefreshToken 存 Redis,可吊销。npm install passport passport-jwt passport-localconst passport = require("passport");
const { Strategy: JwtStrategy } = require("passport-jwt");
const { Strategy: LocalStrategy } = require("passport-local");
// 本地用户名密码验证
passport.use(new LocalStrategy({ usernameField: "username" },
(username, password, done) => {
if (username === "admin" && password === "123456") {
return done(null, { id: 1, username });
}
return done(null, false, { message: "用户名或密码错误" });
}
));
// JWT 验证
passport.use(new JwtStrategy(
{ secretOrKey: SECRET, jwtFromRequest: (req) =>
(req.headers.authorization || "").replace("Bearer ", "") },
(payload, done) => done(null, payload)
));
app.use(passport.initialize());
app.post("/login", passport.authenticate("local", { session: false }),
(req, res) => {
const token = jwt.sign({ userId: req.user.id, username: req.user.username }, SECRET);
res.json({ token });
});
app.get("/profile", passport.authenticate("jwt", { session: false }),
(req, res) => res.json({ user: req.user }));Passport 支持 500+ 策略,统一通过 passport.authenticate('strategy') 接入第三方登录(GitHub/微信等)。
在认证之上做角色授权。
// 角色权限映射
const ROLES = {
admin: ["read", "write", "delete", "manage"],
editor: ["read", "write"],
viewer: ["read"],
};
// 授权中间件(从 req.user 读取角色)
const authorize = (...required) => (req, res, next) => {
const permissions = ROLES[req.user?.role] || [];
const ok = required.every((p) => permissions.includes(p));
if (!ok) return res.status(403).json({ error: "无权限" });
next();
};
// 组合使用
app.get("/admin/users",
authMiddleware, // 先认证
authorize("manage"), // 再授权
(req, res) => res.json({ users: [] }));认证 → 授权 → 业务。把 authMiddleware 和 authorize() 做通用化,避免在每个路由重复校验逻辑。
| 风险 | 应对 |
|---|---|
| XSS 窃取 token | Cookie 设 httpOnly;token 尽量放请求头而非 localStorage |
| CSRF | Session 场景必须加 CSRF token;同源校验 |
| 暴力破解 | 登录接口限流(express-rate-limit) |
| 敏感信息外泄 | JWT Payload 与日志中脱敏(`/password |
| 密码存储 | 永远用 bcrypt/argon2,不加盐不可逆 |
| HTTPS | 生产环境强制,secure: true 的 cookie |
// 密码哈希示例(需在 async 函数中调用)
const bcrypt = require("bcrypt");
async function hashPassword(password) {
return bcrypt.hash(password, 10) // 注册
}
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash) // 登录
}缩短有效期 + 用 refresh token + 登录设备管理;必要时维护黑名单。
用户在活跃时主动刷新 access token(滑动过期),或前端定时用 refresh token 换新。