← 中文资源

简体中文版本

Fastify JWT 认证守卫

用 @fastify/jwt 插件保护路由:preHandler 守卫验证令牌并校验角色,声明挂到请求对象并带类型,处理器零样板。

阅读英文原文

示例说明

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));