Express API 权限设计:认证、角色授权与资源归属
本文目录
登录成功只能说明请求关联到一个身份,不代表这个身份可以执行任意操作。可靠的 API 需要把身份认证、角色或权限判断、具体资源的归属校验分开处理,并在服务端的每一次请求中执行授权。
三层判断
身份认证
认证回答“请求来自谁”。使用令牌时,服务端应验证签名、签发者、受众和有效期,并按需要确认账号仍处于可用状态。只解码 JWT 而不验证签名,或仅相信客户端传来的用户 ID,都不能建立可信身份。
操作授权
授权回答“这个身份可以做什么”。角色可以表达较粗粒度的能力,例如管理员可以管理目录,普通读者只能读取公开目录。不要把按钮隐藏、前端路由守卫或本地存储中的角色值当作安全边界。
资源授权
拥有某个角色也不一定可以访问任意一条记录。订单、文件、草稿等资源还需要检查所有者、组织或显式共享关系。应从服务端已验证的身份取得用户 ID,再和数据库中的资源关系比较,而不是采用请求体中由客户端自行声明的所有者。
将公开路由与管理路由分开
Express Router 可以让每组路由明确声明共同的授权前置条件。下面是结构示意,具体错误处理和类型可按项目约定补齐:
const publicRouter = Router();
publicRouter.get('/catalog', listPublicCatalog);
const adminRouter = Router();
adminRouter.use(authenticate, requireRole('ADMIN'));
adminRouter.get('/catalog', listAllCatalogItems);
adminRouter.post('/catalog', createCatalogItem);
adminRouter.patch('/catalog/:id', updateCatalogItem);
adminRouter.delete('/catalog/:id', deleteCatalogItem);
app.use('/api', publicRouter);
app.use('/api/admin', adminRouter);
路由分组能减少某个新增管理接口意外漏掉授权中间件的机会,但每条路由仍应接受审查和测试。管理查询也属于受保护操作:隐藏记录、审核队列和批量导出不能因为是 GET 就默认公开。
默认拒绝比“新增路由时记得加权限”更稳妥。每个管理入口都应有清晰的策略;遇到未知身份、未知角色、账号停用或授权信息缺失时,服务端不应放行。
对单条资源检查归属
集合级角色判断不能替代对象级授权。示意逻辑如下:
const record = await repository.findById(req.params.id);
if (!record) return res.sendStatus(404);
const isOwner = record.ownerId === req.user.id;
const canManageAll = req.user.permissions.includes('records:manage:any');
if (!isOwner && !canManageAll) return res.sendStatus(403);
return res.json(record);
项目可以根据隐私策略选择对不存在和无权访问的资源统一返回 404,避免泄露资源是否存在。关键是所有读取、更新、删除、下载和批量操作都执行同一套资源级规则,不能只保护详情页。
身份与权限可能变化
长期有效的令牌可能保留过期的角色声明。高权限 API 可以在请求时读取当前账号状态或使用短时会话,并提供撤销与密钥轮换机制。权限变化、账号封禁和退出登录都要有明确的生效策略;不能默认旧令牌会立即失效。
管理员初始化也不应依赖通用默认密码。使用受控的一次性初始化流程,凭据通过独立安全渠道交付,启用后立即撤销初始化入口,并记录管理操作审计日志。
权限回归矩阵
为每个受保护路由至少覆盖以下身份:
| 身份 | 预期结果 |
|---|---|
| 未登录 | 401,或项目约定的未认证响应 |
| 已登录但无所需权限 | 403,或按隐私策略返回 404 |
| 有权限的管理员 | 允许执行且结果符合字段范围 |
| 资源所有者 | 仅允许访问其拥有或获共享的资源 |
| 非所有者普通用户 | 不得读取或修改他人资源 |
| 已停用或权限已撤销的账号 | 按当前账号状态拒绝访问 |
测试应覆盖每种 HTTP 方法、隐藏数据、分页和批量接口,以及重放旧令牌的情况。除了状态码,还要断言数据库没有发生未授权写入,响应体没有泄露受保护字段。
参考资料
本文是通用的工程实践整理,不针对某个私人项目报告具体漏洞,也不表示任何特定系统已完成修复。原文章地址保持不变,以避免已有链接失效。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。