Node.js 独立产品鉴权:先把 Session 生命周期讲清楚
Node.js 独立产品鉴权先把 Session 生命周期讲清楚独立产品做 Node.js 后端时鉴权经常被低估。登录能成功不代表鉴权设计完成。Session 怎么创建、多久过期、如何刷新、如何退出、密码修改后是否失效、多设备如何处理这些都需要明确。鉴权不是一个登录接口而是一套生命周期。一、Session 生命周期flowchart TD A[Login] -- B[Create Session] B -- C[Use Token Or Cookie] C -- D[Refresh Or Expire] D -- E[Logout] D -- F[Revoke]如果只实现登录和校验后面遇到账号泄漏、设备丢失、权限变化就会很难处理。二、Cookie 方案要设置安全属性res.cookie(sid, sessionId, { httpOnly: true, secure: true, sameSite: lax, maxAge: 7 * 24 * 60 * 60 * 1000 });httpOnly可以减少脚本读取风险secure要求 HTTPSsameSite能降低 CSRF 风险。上线产品不要用默认 cookie 配置糊弄。除了这三个属性还有两个容易被忽略的安全 Headerapp.use((req, res, next) { res.setHeader(Strict-Transport-Security, max-age63072000; includeSubDomains); res.setHeader(X-Content-Type-Options, nosniff); next(); });Strict-Transport-SecurityHSTS告诉浏览器始终用 HTTPS 访问防止 SSL 剥离攻击。X-Content-Type-Options: nosniff防止浏览器 MIME 类型猜测导致的 XSS。生产环境的 session cookie建议完整配置如下const SESSION_CONFIG { name: __Host-sid, // __Host- 前缀要求 secure 和 path/ httpOnly: true, // 不可被 JS 读取 secure: true, // 只在 HTTPS 下发送 sameSite: lax as const, // 防御 CSRF path: /, // 全站有效 maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天 };cookie name 使用__Host-前缀是额外的安全措施。浏览器强制要求这种前缀的 cookie 必须带有secure和path/且不能有domain属性。这防止了子域名设置覆盖父域名 cookie 的攻击。三、服务端要能撤销 SessionJWT 纯无状态很方便但撤销困难。独立产品早期用服务端 session 表反而更可控。CREATE TABLE sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, revoked_at TIMESTAMP );退出登录、修改密码、管理员封禁都可以让 session 失效。但如果后续想升级到 JWT建议保留 session 表作为撤销清单CREATE TABLE session_revocations ( user_id TEXT NOT NULL, revoked_before TIMESTAMP NOT NULL, reason TEXT );JWT 校验时查一下这个表如果 token 的签发时间早于revoked_before就视为无效async function validateJWT(token: string): PromiseUser | null { try { const payload jwt.verify(token, JWT_SECRET) as JWTPayload; // 检查是否在撤销期之前 const rev await db.session_revocations.findFirst({ where: { user_id: payload.sub }, orderBy: { revoked_before: desc }, }); if (rev payload.iat * 1000 rev.revoked_before.getTime()) { return null; // token 已被撤销 } return { id: payload.sub }; } catch { return null; } }这样既保留了 JWT 无状态的高性能又能主动撤销。revoked_before时间戳相当于一个开关所有在密码修改之前的 token 都自动失效。四、鉴权中间件要统一async function requireAuth(req, res, next) { const sid req.cookies.sid; const session await sessionStore.findValid(sid); if (!session) return res.status(401).json({ message: 请先登录 }); req.user session.user; next(); }不要在每个接口里手写校验。统一中间件更容易补安全策略。还要区分认证和授权。认证回答你是谁授权回答你能做什么。很多独立产品早期只做登录后来加团队、角色、付费套餐时才发现权限边界没设计。auth_layers: authentication: session valid authorization: role or plan allowed audit: sensitive action recorded比如删除项目、导出数据、邀请成员都应该做授权检查和审计记录。权限系统可以从简单的角色模型开始enum Role { OWNER owner, ADMIN admin, MEMBER member, VIEWER viewer, } const ROLE_PERMISSIONS: RecordRole, string[] { owner: [*], admin: [project:write, member:invite, billing:view], member: [project:write], viewer: [project:read], }; function hasPermission(user: User, action: string): boolean { const permissions ROLE_PERMISSIONS[user.role]; return permissions.includes(*) || permissions.includes(action); } // 中间件 function requirePermission(action: string) { return (req, res, next) { if (!hasPermission(req.user, action)) { return res.status(403).json({ message: 无权执行此操作 }); } next(); }; } // 路由使用 app.delete(/api/projects/:id, requireAuth, requirePermission(project:write), deleteProject);这套模型简单但足以覆盖大多数独立产品的权限需求。后续可以升级到更细粒度的 ABAC 或 RBAC但数据结构和接口契约保持兼容。审计记录建议用简单的日志表CREATE TABLE audit_logs ( id SERIAL PRIMARY KEY, user_id TEXT NOT NULL, action TEXT NOT NULL, resource TEXT NOT NULL, ip TEXT, user_agent TEXT, created_at TIMESTAMP DEFAULT NOW() );关键操作删除、导出、权限变更写入审计日志。体量再小的产品也应该有。一个容易被忽略的 Session 管理细节是滑动过期。很多产品设了 7 天过期但用户在 7 天内一直活跃突然就被踢出——因为过期是固定时间点计算而不是每次请求后自动刷新。推荐做法是每次校验 Session 时将expires_at向后滑动一段时间// 每次校验时自动刷新过期时间 async function findValid(sid: string) { const session await db.sessions.findFirst({ where: { id: sid, expires_at: { gt: new Date() }, revoked_at: null }, }); if (!session) return null; // 滑动过期距离过期不足 1 天时自动续期 const oneDay 24 * 60 * 60 * 1000; if (session.expires_at.getTime() - Date.now() oneDay) { await db.sessions.update({ where: { id: sid }, data: { expires_at: new Date(Date.now() 7 * oneDay) }, }); } return session; }这样活跃用户的 Session 不会无故过期而不活跃用户的 Session 按预期失效。给用户展示活跃设备列表时last_seen_at能帮助用户判断是否有异常登录。安全性不只在防御外部攻击也在于让用户能审视自己的会话状态。五、总结Node.js 独立产品做鉴权要先讲清 Session 生命周期创建、使用、过期、刷新、退出和撤销。Cookie 安全属性、服务端 session 表和统一中间件都要认真设计。登录只是入口。能安全地管理会话产品才算有基本防线。鉴权做得早不会明显增加产品卖点但会减少以后补安全债的痛苦。独立产品越小越不能靠人工记忆守边界。Session 表还可以记录设备、IP 和 user agent 摘要方便用户查看登录设备和主动退出。ALTER TABLE sessions ADD COLUMN user_agent TEXT, ADD COLUMN ip_hash TEXT, ADD COLUMN last_seen_at TIMESTAMP;这些信息不一定一开始就展示给用户但记录下来能为后续安全功能留空间。