
本文详解如何在 Fastify 中为路由动态配置 JWT scope 授权,通过装饰器工厂函数实现 fastify.authenticate('admin') 等可传参的鉴权钩子,并确保仅具备指定 scope 的用户才能访问对应接口。
本文详解如何在 fastify 中为路由动态配置 jwt scope 授权,通过装饰器工厂函数实现 `fastify.authenticate('admin')` 等可传参的鉴权钩子,并确保仅具备指定 scope 的用户才能访问对应接口。
在 Fastify 中,身份认证(Authentication)与权限控制(Authorization)需分层设计:前者验证 token 有效性(如 jwtVerify),后者校验用户是否拥有执行某操作所需的权限(如 scope: "read:users")。你当前的 authenticate 装饰器是同步注册的异步钩子,但缺乏参数化能力——无法为不同路由指定差异化的 scope 要求。解决方案是将其重构为装饰器工厂函数(decorator factory):返回一个闭包钩子,捕获 scope 参数并在请求时动态校验。
✅ 正确实现:带 scope 参数的授权装饰器
将 fastify.decorate("authenticate", ...) 改写为工厂函数,返回一个符合 Fastify onRequest 钩子签名的异步函数:
fastify.decorate("authenticate", function (requiredScope) {
return async function (request, reply) {
try {
// 1. 解析并验证 JWT(自动抛出错误若失效)
const decoded = await request.jwtDecode();
// 2. 提取 scope 字段(注意:JWT payload 中 scope 可能是字符串或空格/逗号分隔的字符串)
const { scope } = decoded.payload;
if (!scope) {
throw new Error("Missing 'scope' claim in JWT");
}
// 3. 标准化 scope 为数组(兼容常见格式:'read:user write:user' 或 '["read:user","write:user"]')
let scopes = Array.isArray(scope)
? scope
: typeof scope === 'string'
? scope.trim().split(/[\s,]+/)
: [];
// 4. 检查是否包含必需 scope
if (!scopes.includes(requiredScope)) {
throw new Error(`Insufficient scope: required '${requiredScope}', got [${scopes.join(', ')}]`);
}
// 5. 最终验证签名(确保 token 未被篡改)
await request.jwtVerify();
} catch (err) {
request.log.warn({ err }, 'Authentication failed');
reply.status(403).send({ error: 'Forbidden', message: err.message });
}
};
});? 在路由中使用 scope 授权
现在可为每个路由精准指定所需权限:
// 仅允许拥有 'create:news' scope 的用户发布新闻
fastify.post(
"/",
{
onRequest: [fastify.authenticate('create:news')],
},
async (req, reply) => {
await sem.take(async () => {
try {
const _news = createNews(req.body);
if (_news) await generate(_news);
} finally {
sem.leave();
}
});
}
);
// 管理员专用接口
fastify.delete("/news/:id", {
onRequest: [fastify.authenticate('delete:news')],
}, async (req, reply) => {
// ...
});⚠️ 关键注意事项
-
Scope 解析鲁棒性:OIDC/JWT 中
scope字段格式不统一(空格分隔、逗号分隔、JSON 数组),务必做标准化处理,避免'read news' !== 'read:news'类型误判。 -
错误处理一致性:推荐统一用
reply.status(403)响应权限拒绝,而非401(401 表示未认证,403 表示已认证但无权限)。 -
性能优化:
jwtDecode()和jwtVerify()已内置缓存,无需手动优化;但高并发场景下,确保getJwks.getPublicKey的缓存策略合理(如buildGetJwks默认启用 LRU 缓存)。 -
调试技巧:在钩子中添加
request.log.info({ scope: decoded.payload.scope }),便于排查 scope 解析问题。
✅ 总结
通过将 authenticate 从“静态钩子”升级为“scope 工厂”,你获得了细粒度的路由级授权能力。这种模式完全契合 Fastify 的声明式路由设计哲学:路由定义即权限契约。后续还可扩展支持多 scope(如 ['read:news', 'publish:news'])、scope 前缀匹配(scope.startsWith('admin:'))或 RBAC 角色映射,让权限体系随业务演进而无缝伸缩。


















