概念定义
访问控制分两层:能否使用这个功能,以及能否对“这个对象”使用它。第二层失败得很安静:端点对任何有效会话与任何 id 都返回 200,跨租户读取只需一次 curl。
工作机制
健壮模式是让查询按认证主体圈定范围:WHERE id = :id AND owner_id = :user,或绑定会话变量的数据库行级安全策略。框架中间件与 ORM 支持默认作用域,让处理器想漏查都难。GraphQL 的嵌套路径每个都需强制执行。
使用场景
每个触碰 id 的处理器,包括管理面——内部工具同样泄数据。自动化测试应让两个不同用户请求同一对象,断言第二个拿到 404/403 而非载荷。
常见误区
“认证即授权”不成立;UUID 换掉序列 id 也只防枚举不防越权——id 本就经列表或日志外泄。先取回再过滤会泄漏对象存在性与时序。批量端点里每个 id 都要圈定;客户端能传 owner_id 且模型照单绑定时,mass assignment 与 BOLA 双重叠加。
代码示例
export function createFlagSet(names) {
if (!Array.isArray(names) || names.length === 0) {
throw new TypeError("names must be a non-empty array");
}
if (names.length > 30) throw new RangeError("at most 30 flags are supported");
const bits = new Map();
const all = [];
for (const name of names) {
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("flag names must be non-empty strings");
}
const normalized = name.trim();
if (bits.has(normalized)) throw new Error(`duplicate flag "${normalized}"`);
bits.set(normalized, 1 << bits.size);
all.push(normalized);
}
const resolve = (name) => {
const bit = bits.get(name);
if (bit === undefined) throw new Error(`unknown flag "${name}"`);
return bit;
};
return {
names: () => [...all],
mask(...selected) {
return selected.reduce((mask, name) => mask | resolve(name), 0);
},
has(mask, name) {
return (mask & resolve(name)) !== 0;
},
add(mask, name) {
return mask | resolve(name);
},
remove(mask, name) {
return mask & ~resolve(name);
},
toggle(mask, name) {
return mask ^ resolve(name);
},
toNames(mask) {
return all.filter((name) => (mask & resolve(name)) !== 0);
},
};
}
const permissions = createFlagSet(["read", "write", "admin"]);
let mask = permissions.mask("read", "write");
console.log(mask, permissions.toNames(mask));
mask = permissions.remove(mask, "write");
mask = permissions.toggle(mask, "admin");
console.log(permissions.has(mask, "write"), permissions.toNames(mask));
import express from 'express';
import jwt from 'jsonwebtoken';
function requireAuthMiddleware(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 required' });
try {
req.claims = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(403).json({ error: 'invalid token' });
}
}
function requireRoleMiddleware(...allowed) {
return (req, res, next) => {
const role = req.claims?.role;
if (!role || !allowed.includes(role)) {
return res.status(403).json({ error: 'role ' + role + ' not permitted' });
}
next();
};
}
const app = express();
app.delete('/invoices/:id',
requireAuthMiddleware,
requireRoleMiddleware('admin', 'finance'),
(req, res) => res.json({ deleted: req.params.id }),
);
app.get('/profile',
requireAuthMiddleware,
(req, res) => res.json({ userId: req.claims.sub }),
);
app.listen(3000);