express-jwt vs express-jwt-authz vs jsonwebtoken vs jwt-simple vs koa-jwt vs passport-jwt
Node.js 应用中 JWT 认证与授权库的深度技术选型
express-jwtexpress-jwt-authzjsonwebtokenjwt-simplekoa-jwtpassport-jwt类似的npm包:

Node.js 应用中 JWT 认证与授权库的深度技术选型

本文对比了 Node.js 生态中处理 JSON Web Tokens (JWT) 的六大核心库。jsonwebtoken 是行业标准,负责 Token 的生成与验证核心逻辑;jwt-simple 是一个轻量级但已不再推荐的替代方案。express-jwt 和 koa-jwt 分别是针对 Express 和 Koa 框架的中间件,用于自动处理请求中的 Token 验证。express-jwt-authz 专注于基于权限 scopes 的授权检查。passport-jwt 则是 Passport.js 策略的一部分,适合需要多种认证方式(如本地 + JWT + OAuth)集成的复杂场景。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
express-jwt04,51428.5 kB642 年前MIT
express-jwt-authz0987.75 kB10-MIT
jsonwebtoken018,19043.4 kB2089 个月前MIT
jwt-simple01,357-347 年前MIT
koa-jwt01,35043.2 kB7-MIT
passport-jwt01,97852 kB42-MIT

Node.js JWT 生态全景:从底层加密到框架集成

在构建现代 Node.js 后端时,JSON Web Tokens (JWT) 是无状态认证的事实标准。然而,npm 上众多的 JWT 相关库往往让开发者感到困惑:到底该用哪个?是直接用底层的 jsonwebtoken,还是选择框架专用的中间件?

本文将深入剖析这六个关键库的技术差异,帮助你根据架构需求做出精准选择。

🏗️ 核心层 vs. 框架层:职责分离

首先必须明确一个概念:生成/验证 Token 与 在 HTTP 请求中处理 Token 是两回事。

  • 核心层:负责数学运算(签名/验签)、编解码。代表库:jsonwebtoken, jwt-simple。
  • 框架层:负责从 HTTP Header 提取 Token,调用核心层验证,并将结果注入请求对象。代表库:express-jwt, koa-jwt, passport-jwt。

1. 核心库对决:jsonwebtoken vs. jwt-simple

jsonwebtoken 是目前 Node.js 社区的绝对标准。它由 Auth0 维护,支持所有主流算法(HS256, RS256 等),API 设计严谨。

// jsonwebtoken: 标准用法
const jwt = require('jsonwebtoken');
const secret = 'my-secret';

// 签发
const token = jwt.sign({ userId: 123, role: 'admin' }, secret, { expiresIn: '1h' });

// 验证
try {
  const decoded = jwt.verify(token, secret);
  console.log(decoded.userId); // 123
} catch (err) {
  // 处理过期或无效签名
  console.error('Invalid token');
}

jwt-simple 曾因其极简 API 而流行,但近年来更新缓慢,功能集远不如 jsonwebtoken 丰富(例如对某些算法的支持较弱)。

// jwt-simple: 简易用法
const jwt = require('jwt-simple');
const secret = 'my-secret';

// 签发
const token = jwt.encode({ userId: 123 }, secret);

// 验证
try {
  const decoded = jwt.decode(token, secret);
  console.log(decoded.userId);
} catch (err) {
  console.error('Invalid token');
}

🛑 选型建议:在新项目中永远选择 jsonwebtoken。jwt-simple 已不再适合作为生产环境的核心依赖,其生态位已被完全取代。

🚦 框架中间件:自动化请求处理

一旦有了 Token,我们需要在每次 API 请求中验证它。手写提取 Header 和调用 jwt.verify 会很繁琐,因此有了框架专用中间件。

2. Express 生态:express-jwt

express-jwt 是 Express 开发者的首选。它自动从 Authorization: Bearer <token> 头中提取 Token,验证通过后会将解码后的 payload 挂载到 req.user。

// express-jwt: 中间件集成
const express = require('express');
const { expressjwt: jwt } = require('express-jwt'); // 注意 v8+ 的导入方式变化
const app = express();

app.use(
  jwt({
    secret: 'my-secret',
    algorithms: ['HS256'],
    requestProperty: 'user' // 默认挂载到 req.user
  })
);

app.get('/profile', (req, res) => {
  // 如果代码执行到这里,说明 Token 验证已通过
  res.json({ user: req.user }); 
});

// 全局错误处理捕获验证失败
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    res.status(401).send('Invalid token');
  }
});

3. Koa 生态:koa-jwt

Koa 的中间件机制基于 async/await 和 Context (ctx),因此不能直接使用 Express 中间件。koa-jwt 专为 Koa 设计,行为逻辑类似,但操作的是 ctx.state。

// koa-jwt: 中间件集成
const Koa = require('koa');
const jwt = require('koa-jwt');
const app = new Koa();

app.use(
  jwt({
    secret: 'my-secret',
    key: 'state' // 默认将 payload 挂载到 ctx.state.user
  }).unless({ path: ['/public'] }) // 内置忽略路径功能
);

app.use(async (ctx) => {
  // 验证通过后,数据在 ctx.state.user
  ctx.body = { user: ctx.state.user };
});

🛑 选型建议:严格根据你的 Web 框架选择。Express 用 express-jwt,Koa 用 koa-jwt。不要混用,否则会导致对象挂载位置错误(req vs ctx)。

🔐 授权与策略集成:更复杂的场景

仅仅知道“用户是谁”(认证)往往不够,我们还需要知道“用户能做什么”(授权),或者需要统一管理多种登录方式。

4. 细粒度授权:express-jwt-authz

express-jwt 只负责验证 Token 的有效性。如果你的 Token 中包含 scope 字段(例如 ['read:users', 'write:posts']),你需要 express-jwt-authz 来检查这些权限。

它通常与 express-jwt 链式使用。

// express-jwt-authz: 权限检查
const { authz } = require('express-jwt-authz');

// 假设前一个中间件已经验证了 Token 并将 payload 放入 req.user
// 且 payload 中包含 scopes: ['read:users']

app.get('/users', 
  authz(['read:users']), // 检查是否拥有 read:users 权限
  (req, res) => {
    res.json({ data: 'Sensitive User Data' });
  }
);

// 如果用户没有该 scope,此中间件会直接返回 403 Forbidden

5. 统一认证策略:passport-jwt

如果你的应用不仅支持 JWT,还支持本地账号密码、GitHub 登录等多种方式,passport-jwt 是最佳选择。它是 Passport.js 生态系统的一部分,允许你通过统一的 passport.authenticate 接口管理所有策略。

// passport-jwt: 策略集成
const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');

const options = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: 'my-secret'
};

passport.use(new JwtStrategy(options, (jwt_payload, done) => {
  // 这里可以额外查询数据库确认用户状态
  User.findById(jwt_payload.id, (err, user) => {
    if (err) return done(err, false);
    if (user) return done(null, user);
    return done(null, false);
  });
}));

app.get('/dashboard', 
  passport.authenticate('jwt', { session: false }), 
  (req, res) => {
    res.json(req.user);
  }
);

🛑 选型建议:

  • 如果只需要单纯的 JWT API 保护,express-jwt 更轻量、配置更简单。
  • 如果需要混合认证模式(JWT + OAuth + Local)或利用 Passport 的插件生态,选择 passport-jwt。

📊 核心特性对比总结

特性jsonwebtokenexpress-jwtkoa-jwtpassport-jwtexpress-jwt-authz
主要职责生成/验证 TokenExpress 请求验证Koa 请求验证Passport 策略集成权限 (Scope) 检查
框架依赖无 (通用)ExpressKoaPassport (Express/Koa)Express
数据挂载返回值req.userctx.state.userreq.user无 (仅校验)
适用场景底层工具库纯 Express API纯 Koa API多策略认证系统细粒度权限控制

💡 架构师的最佳实践组合

在实际的企业级开发中,我们通常会看到以下组合模式:

  1. 标准 REST API (Express):

    • 使用 jsonwebtoken 在登录接口生成 Token。
    • 使用 express-jwt 保护受路由。
    • 使用 express-jwt-authz 保护敏感操作(如删除资源)。
    // 组合示例
    app.post('/login', (req, res) => {
      // 使用核心库生成
      const token = jwt.sign({ sub: user.id, scope: ['read'] }, secret);
      res.json({ token });
    });
    
    app.delete('/users/:id', 
      expressJwt({ secret }), // 1. 验证身份
      authz(['delete:users']), // 2. 验证权限
      deleteUserController
    );
    
  2. 微服务或内部工具:

    • 如果不需要复杂的权限 scopes,仅验证身份,单独使用 express-jwt 或 koa-jwt 即可,无需引入 passport 的重量级配置。
  3. 遗留系统迁移:

    • 如果你看到 jwt-simple,请制定计划将其重构为 jsonwebtoken,以避免潜在的安全算法缺失风险。

🎯 最终结论

不要为了“功能多”而盲目选择最复杂的库。

  • 做数学题(签发/验签):选 jsonwebtoken。
  • 做Express 守卫:选 express-jwt。
  • 做Koa 守卫:选 koa-jwt。
  • 做权限检查:加选 express-jwt-authz。
  • 做大杂烩认证:选 passport-jwt。
  • 避开 jwt-simple。

理解每个库的边界,将它们像乐高积木一样组合,才能构建出既安全又易于维护的认证架构。

如何选择: express-jwt vs express-jwt-authz vs jsonwebtoken vs jwt-simple vs koa-jwt vs passport-jwt

  • express-jwt:

    如果你的项目基于 Express 框架,且只需要标准的 Bearer Token 验证并将用户信息挂载到 req.user,这是最直接、性能最好的中间件方案。

  • express-jwt-authz:

    当你已经完成了身份验证(知道用户是谁),还需要检查用户是否拥有特定权限(scopes/permissions)才能访问某个资源时,配合 express-jwt 一起使用。

  • jsonwebtoken:

    如果你需要手动控制 Token 的签发(sign)和验证(verify)逻辑,或者你的框架没有专用的 JWT 中间件,这是唯一推荐的选择。它是目前社区事实上的标准,API 稳定且功能最全。

  • jwt-simple:

    除非你在维护极其古老的遗留代码且无法升级,否则不要在新项目中使用此库。它缺乏对现代算法的完整支持,且社区已转向 jsonwebtoken。

  • koa-jwt:

    专为 Koa 框架设计。如果你的技术栈是 Koa,请使用此库而非 Express 的中间件,因为它完美适配 Koa 的 Context 对象和中间件执行模型。

  • passport-jwt:

    如果你的应用需要同时支持多种认证策略(例如:用户名密码登录、JWT、Google OAuth 等),并且希望统一通过 Passport 的 authenticate 中间件管理会话,这是最佳集成方案。

express-jwt的README

express-jwt

This module provides Express middleware for validating JWTs (JSON Web Tokens) through the jsonwebtoken module. The decoded JWT payload is available on the request object.

Install

$ npm install express-jwt

API

expressjwt(options)

Options has the following parameters:

  • secret: jwt.Secret | GetVerificationKey (required): The secret as a string or a function to retrieve the secret.
  • getToken?: TokenGetter (optional): A function that receives the express Request and returns the token, by default it looks in the Authorization header.
  • isRevoked?: IsRevoked (optional): A function to verify if a token is revoked.
  • onExpired?: ExpirationHandler (optional): A function to handle expired tokens.
  • credentialsRequired?: boolean (optional): If its false, continue to the next middleware if the request does not contain a token instead of failing, defaults to true.
  • requestProperty?: string (optional): Name of the property in the request object where the payload is set. Default to req.auth.
  • Plus... all the options available in the jsonwebtoken verify function.

The available functions have the following interface:

  • GetVerificationKey = (req: express.Request, token: jwt.Jwt | undefined) => Promise<jwt.Secret>;
  • IsRevoked = (req: express.Request, token: jwt.Jwt | undefined) => Promise<boolean>;
  • TokenGetter = (req: express.Request) => string | Promise<string> | undefined;

Usage

Basic usage using an HS256 secret:

var { expressjwt: jwt } = require("express-jwt");
// or ES6
// import { expressjwt, ExpressJwtRequest } from "express-jwt";

app.get(
  "/protected",
  jwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }),
  function (req, res) {
    if (!req.auth.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

The decoded JWT payload is available on the request via the auth property.

The default behavior of the module is to extract the JWT from the Authorization header as an OAuth2 Bearer token.

Required Parameters

The algorithms parameter is required to prevent potential downgrade attacks when providing third party libraries as secrets.

:warning: Do not mix symmetric and asymmetric (ie HS256/RS256) algorithms: Mixing algorithms without further validation can potentially result in downgrade vulnerabilities.

jwt({
  secret: "shhhhhhared-secret",
  algorithms: ["HS256"],
  //algorithms: ['RS256']
});

Additional Options

You can specify audience and/or issuer as well, which is highly recommended for security purposes:

jwt({
  secret: "shhhhhhared-secret",
  audience: "http://myapi/protected",
  issuer: "http://issuer",
  algorithms: ["HS256"],
});

If the JWT has an expiration (exp), it will be checked.

If you are using a base64 URL-encoded secret, pass a Buffer with base64 encoding as the secret instead of a string:

jwt({
  secret: Buffer.from("shhhhhhared-secret", "base64"),
  algorithms: ["RS256"],
});

To only protect specific paths (e.g. beginning with /api), use express router call use, like so:

app.use("/api", jwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }));

Or, the other way around, if you want to make some paths unprotected, call unless like so.

app.use(
  jwt({
    secret: "shhhhhhared-secret",
    algorithms: ["HS256"],
  }).unless({ path: ["/token"] })
);

This is especially useful when applying to multiple routes. In the example above, path can be a string, a regexp, or an array of any of those.

For more details on the .unless syntax including additional options, please see express-unless.

This module also support tokens signed with public/private key pairs. Instead of a secret, you can specify a Buffer with the public key

var publicKey = fs.readFileSync("/path/to/public.pub");
jwt({ secret: publicKey, algorithms: ["RS256"] });

Customizing Token Location

A custom function for extracting the token from a request can be specified with the getToken option. This is useful if you need to pass the token through a query parameter or a cookie. You can throw an error in this function and it will be handled by express-jwt.

app.use(
  jwt({
    secret: "hello world !",
    algorithms: ["HS256"],
    credentialsRequired: false,
    getToken: function fromHeaderOrQuerystring(req) {
      if (
        req.headers.authorization &&
        req.headers.authorization.split(" ")[0] === "Bearer"
      ) {
        return req.headers.authorization.split(" ")[1];
      } else if (req.query && req.query.token) {
        return req.query.token;
      }
      return null;
    },
  })
);

Retrieve key dynamically

If you need to obtain the key dynamically from other sources, you can pass a function in the secret parameter with the following parameters:

  • req (Object) - The express request object.
  • token (Object) - An object with the JWT payload and headers.

For example, if the secret varies based on the issuer:

var jwt = require("express-jwt");
var data = require("./data");
var utilities = require("./utilities");

var getSecret = async function (req, token) {
  const issuer = token.payload.iss;
  const tenant = await data.getTenantByIdentifier(issuer);
  if (!tenant) {
    throw new Error("missing_secret");
  }
  return utilities.decrypt(tenant.secret);
};

app.get(
  "/protected",
  jwt({ secret: getSecret, algorithms: ["HS256"] }),
  function (req, res) {
    if (!req.auth.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

Secret rotation

The getSecret callback could also be used in cases where the same issuer might issue tokens with different keys at certain point:

var getSecret = async function (req, token) {
  const { iss } = token.payload;
  const { kid } = token.header;
  // get the verification key by a given key-id and issuer.
  return verificationKey;
};

Revoked tokens

It is possible that some tokens will need to be revoked so they cannot be used any longer. You can provide a function as the isRevoked option. The signature of the function is function(req, payload, done):

  • req (Object) - The express request object.
  • token (Object) - An object with the JWT payload and headers.

For example, if the (iss, jti) claim pair is used to identify a JWT:

const jwt = require("express-jwt");
const data = require("./data");

const isRevokedCallback = async (req, token) => {
  const issuer = token.payload.iss;
  const tokenId = token.payload.jti;
  const token = await data.getRevokedToken(issuer, tokenId);
  return token !== "undefined";
};

app.get(
  "/protected",
  jwt({
    secret: "shhhhhhared-secret",
    algorithms: ["HS256"],
    isRevoked: isRevokedCallback,
  }),
  function (req, res) {
    if (!req.auth.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

Handling expired tokens

You can handle expired tokens as follows:

  jwt({
    secret: "shhhhhhared-secret",
    algorithms: ["HS256"],
    onExpired: async (req, err) => {
      if (new Date() - err.inner.expiredAt < 5000) { return;}
      throw err;
    },,
  })

Error handling

The default behavior is to throw an error when the token is invalid, so you can add your custom logic to manage unauthorized access as follows:

app.use(function (err, req, res, next) {
  if (err.name === "UnauthorizedError") {
    res.status(401).send("invalid token...");
  } else {
    next(err);
  }
});

You might want to use this module to identify registered users while still providing access to unregistered users. You can do this by using the option credentialsRequired:

app.use(
  jwt({
    secret: "hello world !",
    algorithms: ["HS256"],
    credentialsRequired: false,
  })
);

Typescript

A Request type is provided from express-jwt, which extends express.Request with the auth property. It could be aliased, like how JWTRequest is below.

import { expressjwt, Request as JWTRequest } from "express-jwt";

app.get(
  "/protected",
  expressjwt({ secret: "shhhhhhared-secret", algorithms: ["HS256"] }),
  function (req: JWTRequest, res: express.Response) {
    if (!req.auth?.admin) return res.sendStatus(401);
    res.sendStatus(200);
  }
);

Migration from v6

  1. The middleware function is now available as a named import rather than a default one: import { expressjwt } from 'express-jwt'
  2. The decoded JWT payload is now available as req.auth rather than req.user
  3. The secret function had (req, header, payload, cb), now it can return a promise and receives (req, token). token has header and payload.
  4. The isRevoked function had (req, payload, cb), now it can return a promise and receives (req, token). token has header and payload.

Related Modules

Tests

$ npm install
$ npm test

Contributors

Check them out here

Issue Reporting

If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.

Author

Auth0

License

This project is licensed under the MIT license. See the LICENSE file for more info.