示例说明
JWT 插件把签发与验证装进 Fastify 生态:jwt.sign 生成令牌,jwtVerify 校验并自动把声明挂载到请求上。通过模块扩充声明把 user 字段类型化,处理器里直接访问且带智能提示。守卫函数按路由声明所需角色,鉴权失败区分 401 未认证与 403 权限不足两种语义。
技术要点
- 模块扩充为请求声明提供类型
- preHandler 守卫按角色白名单校验
- 401 与 403 语义分离
代码解读
requireRole 返回 preHandler 钩子:先验证令牌捕获失败回 401,再检查角色包含关系不满足回 403。两个路由分别要求任意已登录用户与管理员。app.inject 的内存调用携带签发的令牌验证全链路,无需真实网络监听。
使用建议
密钥从环境注入并在生产使用强随机值。令牌保持短时效,刷新走独立端点。角色枚举集中定义防漂移。
代码示例
import Fastify from 'fastify';
import jwt from '@fastify/jwt';
interface TokenClaims {
sub: string;
role: 'admin' | 'editor' | 'viewer';
}
declare module 'fastify' {
interface FastifyRequest {
user: TokenClaims;
}
}
const app = Fastify({ logger: true });
await app.register(jwt, {
secret: process.env.JWT_SECRET ?? 'dev-secret-change-me',
sign: { expiresIn: '15m' },
});
function requireRole(...roles: TokenClaims['role'][]) {
return async (req: Fastify.FastifyRequest, reply: Fastify.FastifyReply) => {
try {
await req.jwtVerify<TokenClaims>();
} catch {
return reply.code(401).send({ error: 'invalid or expired token' });
}
if (!roles.includes(req.user.role)) {
return reply.code(403).send({ error: 'insufficient role' });
}
};
}
app.get('/me', { preHandler: requireRole('viewer', 'editor', 'admin') },
async (req) => ({ userId: req.user.sub, role: req.user.role }),
);
app.post('/admin/purge', { preHandler: requireRole('admin') },
async (req, reply) => reply.code(202).send({ purged: true, by: req.user.sub }),
);
app.inject({
method: 'GET',
url: '/me',
headers: { authorization: 'Bearer ' + app.jwt.sign({ sub: 'u-1', role: 'viewer' }) },
}).then((res) => console.log('status:', res.statusCode));