jsonwebtoken vs koa-jwt vs koa-passport vs passport-jwt
Node.js JWT 认证方案架构选型与实现对比
jsonwebtokenkoa-jwtkoa-passportpassport-jwt类似的npm包:

Node.js JWT 认证方案架构选型与实现对比

jsonwebtoken 是底层的 JWT 生成与验证核心库,不依赖任何 Web 框架。koa-jwt 是专为 Koa 框架设计的中间件,直接集成 JWT 验证逻辑。passport-jwt 是 Passport 认证库的 JWT 策略插件,提供标准化的认证接口。koa-passport 则是将 Passport 生态引入 Koa 框架的适配器。这四个包分别代表了从底层原语到框架特定中间件,再到通用认证生态的不同抽象层级。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
jsonwebtoken018,19043.4 kB2089 个月前MIT
koa-jwt01,35043.2 kB7-MIT
koa-passport077117.1 kB134 年前MIT
passport-jwt01,97952 kB42-MIT

Node.js JWT 认证方案架构选型与实现对比

在 Node.js 后端开发中,JSON Web Token (JWT) 是无状态认证的事实标准。然而,实现 JWT 认证有多种路径,从底层库到框架中间件,再到通用认证生态。jsonwebtoken、koa-jwt、koa-passport 和 passport-jwt 代表了四种不同的抽象层级。作为架构师,理解它们的边界和适用场景至关重要。

🏗️ 抽象层级:底层原语 vs 框架中间件

jsonwebtoken 是最底层的工具库。它只负责一件事:生成(sign)和验证(verify)Token。它不知道什么是 HTTP 请求,也不知道什么是 Koa 或 Express。

// jsonwebtoken: 纯工具函数
const jwt = require('jsonwebtoken');

// 生成 Token
const token = jwt.sign({ userId: 123 }, 'SECRET_KEY', { expiresIn: '1h' });

// 验证 Token
const decoded = jwt.verify(token, 'SECRET_KEY');

koa-jwt 是 Koa 框架的专用中间件。它内部封装了 jsonwebtoken,并处理了从 HTTP 请求头提取 Token 的逻辑。

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

app.use(jwt({ secret: 'SECRET_KEY' }).unless({ path: ['/public'] }));

// 验证通过后,ctx.state.user 会被自动填充
app.use(async (ctx) => {
  ctx.body = ctx.state.user;
});

passport-jwt 是 Passport 认证框架的一个策略(Strategy)。它不直接作为中间件使用,而是需要配合 Passport 的核心库初始化。

// passport-jwt: 策略定义
const JwtStrategy = require('passport-jwt').Strategy;

passport.use(new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: 'SECRET_KEY'
  },
  async (payload, done) => {
    // 这里可以查找数据库用户
    return done(null, payload);
  }
));

koa-passport 是连接 Koa 和 Passport 的桥梁。它让 Passport 的策略(包括 passport-jwt)能在 Koa 的中间件链中运行。

// koa-passport: 框架适配器
const passport = require('koa-passport');

// 初始化后,作为中间件使用
app.use(passport.initialize());
app.use(passport.authenticate('jwt', { session: false }));

🔌 框架耦合度:专用 vs 通用

框架耦合度决定了你的代码迁移成本。

  • jsonwebtoken 零耦合。你可以把它用在 Koa、Express、Fastify 甚至纯 Node.js 脚本中。迁移框架时,核心逻辑无需修改。
  • koa-jwt 强耦合 Koa。如果你的项目从 Koa 迁移到 Express,这个包就无法使用了,必须重写中间件逻辑。
  • passport-jwt 弱耦合。它依赖于 Passport 核心,但 Passport 支持多种框架。不过,配置方式依然受限于 Passport 的回调风格。
  • koa-passport 强耦合 Koa + Passport。这是双重依赖,迁移成本最高,但换来了生态的统一性。

🛡️ 认证流程控制:自动 vs 手动

不同的包对认证失败的处理方式不同,这直接影响错误响应的格式。

koa-jwt 默认在验证失败时直接抛出 401 错误,中断请求链。虽然可以捕获,但默认行为比较强硬。

// koa-jwt: 默认拦截
// 如果 Token 无效,直接返回 401 Unauthorized
// 开发者需要额外配置 errorMiddleware 来统一处理

passport-jwt 配合 koa-passport 使用时,允许更灵活的控制。你可以通过 authenticate 的参数决定是否失败时立即终止。

// koa-passport + passport-jwt: 灵活控制
// failureFlash 或 failureRedirect 等选项可用
app.use(passport.authenticate('jwt', { session: false, failWithError: true }));

// 可以在后续中间件统一处理 error

jsonwebtoken 完全手动。你需要自己 try/catch 验证过程,并决定返回什么状态码。

// jsonwebtoken: 手动控制
try {
  const decoded = jwt.verify(token, 'SECRET');
  ctx.state.user = decoded;
  await next();
} catch (err) {
  ctx.status = 401;
  ctx.body = { message: 'Invalid token' };
}

🔄 扩展性:单一认证 vs 多策略混合

当项目需要支持多种登录方式(如 JWT + GitHub OAuth + 账号密码)时,架构选择变得关键。

  • jsonwebtoken + koa-jwt: 适合单一 JWT 认证场景。如果要加 OAuth,你需要引入其他库,导致认证逻辑分散。
  • passport-jwt + koa-passport: 这是 Passport 的强项。你可以同时注册 LocalStrategy、GithubStrategy 和 JwtStrategy,并通过统一的 passport.authenticate 接口调用。
// passport 生态:统一接口
// 本地登录
app.use(passport.authenticate('local', { session: false }));

// JWT 保护的路由
app.use(passport.authenticate('jwt', { session: false }));

// GitHub 登录
app.use(passport.authenticate('github', { scope: ['user:email'] }));

⚠️ 维护状态与潜在风险

在选择包时,必须考虑长期维护性。

  • jsonwebtoken: 由 Auth0 维护,行业标准,更新频繁,安全性高。是其他包的基础依赖。
  • koa-jwt: 社区维护,相对稳定,但功能迭代较慢。适合简单场景。
  • passport 生态 (koa-passport, passport-jwt): Passport 是一个非常成熟的生态,但架构较老(基于回调)。在现代 async/await 普及的今天,有时需要额外的适配代码(如 promisify)来避免回调地狱。不过 koa-passport 已经较好地处理了这一点。

📊 核心差异总结

特性jsonwebtokenkoa-jwtpassport-jwtkoa-passport
定位底层工具库Koa 中间件Passport 策略Koa 适配器
框架依赖无Koa无 (需 Passport 核心)Koa + Passport
配置复杂度低低中高
多策略支持❌ 需自行实现❌ 仅 JWT✅ 支持所有 Passport 策略✅ 支持所有 Passport 策略
代码风格Promise/AsyncMiddlewareCallback/StrategyMiddleware

💡 架构师建议

场景一:轻量级 Koa API 服务 如果你的团队熟悉 Koa,且只需要简单的 JWT 保护 API,koa-jwt 是最高效的选择。它减少了样板代码,让团队专注于业务逻辑。

场景二:复杂认证系统 如果系统需要支持多种登录方式(社交登录、MFA、API Key 等),或者团队已经熟悉 Passport 生态,koa-passport + passport-jwt 是更稳健的选择。虽然初期配置多,但长期维护性更好。

场景三:底层库或微服务 如果你在编写一个被其他项目引用的 SDK,或者在 Serverless 函数中处理 Token,jsonwebtoken 是唯一选择。保持依赖最小化,避免将框架耦合进底层逻辑。

最终结论:没有绝对的“最好”,只有“最合适”。对于大多数现代 Koa 项目,如果认证需求简单,koa-jwt 足够;如果认证是核心复杂业务,Passport 生态提供的标准化流程值得投入。而 jsonwebtoken 则是所有方案背后不可或缺的基石。

如何选择: jsonwebtoken vs koa-jwt vs koa-passport vs passport-jwt

  • jsonwebtoken:

    如果你需要完全控制认证流程,或者在非标准 HTTP 环境(如 WebSocket、微服务内部通信)中使用 JWT,选择 jsonwebtoken。它不提供中间件,你需要自己编写解析请求和验证逻辑,适合构建自定义认证系统或底层库。

  • koa-jwt:

    如果你的项目基于 Koa 框架,且只需要简单的 JWT 验证功能,选择 koa-jwt。它轻量且直接,无需引入庞大的 Passport 生态,适合快速构建 API 服务,但功能相对单一,缺乏复杂的认证流程管理。

  • koa-passport:

    如果你已经在 Koa 项目中使用了 Passport 的其他策略(如 OAuth、本地登录),或者需要统一的认证接口管理多种登录方式,选择 koa-passport。它允许你在 Koa 中复用 Passport 生态,但配置相对繁琐。

  • passport-jwt:

    如果你使用的是 Express 或其他支持 Passport 的框架,或者希望认证逻辑与 Web 框架解耦,选择 passport-jwt。它通常与 koa-passport 或 passport 配合使用,适合需要标准化策略模式的大型项目。

jsonwebtoken的README

jsonwebtoken

BuildDependency
Build StatusDependency Status

An implementation of JSON Web Tokens.

This was developed against draft-ietf-oauth-json-web-token-08. It makes use of node-jws

Install

$ npm install jsonwebtoken

Migration notes

Usage

jwt.sign(payload, secretOrPrivateKey, [options, callback])

(Asynchronous) If a callback is supplied, the callback is called with the err or the JWT.

(Synchronous) Returns the JsonWebToken as string

payload could be an object literal, buffer or string representing valid JSON.

Please note that exp or any other claim is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity.

If payload is not a buffer or a string, it will be coerced into a string using JSON.stringify.

secretOrPrivateKey is a string (utf-8 encoded), buffer, object, or KeyObject containing either the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA. In case of a private key with passphrase an object { key, passphrase } can be used (based on crypto documentation), in this case be sure you pass the algorithm option. When signing with RSA algorithms the minimum modulus length is 2048 except when the allowInsecureKeySizes option is set to true. Private keys below this size will be rejected with an error.

options:

  • algorithm (default: HS256)
  • expiresIn: expressed in seconds or a string describing a time span vercel/ms.

    Eg: 60, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms").

  • notBefore: expressed in seconds or a string describing a time span vercel/ms.

    Eg: 60, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms").

  • audience
  • issuer
  • jwtid
  • subject
  • noTimestamp
  • header
  • keyid
  • mutatePayload: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token.
  • allowInsecureKeySizes: if true allows private keys with a modulus below 2048 to be used for RSA
  • allowInvalidAsymmetricKeyTypes: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided.

There are no default values for expiresIn, notBefore, audience, subject, issuer. These claims can also be provided in the payload directly with exp, nbf, aud, sub and iss respectively, but you can't include in both places.

Remember that exp, nbf and iat are NumericDate, see related Token Expiration (exp claim)

The header can be customized via the options.header object.

Generated jwts will include an iat (issued at) claim by default unless noTimestamp is specified. If iat is inserted in the payload, it will be used instead of the real timestamp for calculating other things like exp given a timespan in options.expiresIn.

Synchronous Sign with default (HMAC SHA256)

var jwt = require('jsonwebtoken');
var token = jwt.sign({ foo: 'bar' }, 'shhhhh');

Synchronous Sign with RSA SHA256

// sign with RSA SHA256
var privateKey = fs.readFileSync('private.key');
var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' });

Sign asynchronously

jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function(err, token) {
  console.log(token);
});

Backdate a jwt 30 seconds

var older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'shhhhh');

Token Expiration (exp claim)

The standard for JWT defines an exp claim for expiration. The expiration is represented as a NumericDate:

A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. This is equivalent to the IEEE Std 1003.1, 2013 Edition [POSIX.1] definition "Seconds Since the Epoch", in which each day is accounted for by exactly 86400 seconds, other than that non-integer values can be represented. See RFC 3339 [RFC3339] for details regarding date/times in general and UTC in particular.

This means that the exp field should contain the number of seconds since the epoch.

Signing a token with 1 hour of expiration:

jwt.sign({
  exp: Math.floor(Date.now() / 1000) + (60 * 60),
  data: 'foobar'
}, 'secret');

Another way to generate a token like this with this library is:

jwt.sign({
  data: 'foobar'
}, 'secret', { expiresIn: 60 * 60 });

//or even better:

jwt.sign({
  data: 'foobar'
}, 'secret', { expiresIn: '1h' });

jwt.verify(token, secretOrPublicKey, [options, callback])

(Asynchronous) If a callback is supplied, function acts asynchronously. The callback is called with the decoded payload if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will be called with the error.

(Synchronous) If a callback is not supplied, function acts synchronously. Returns the payload decoded if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will throw the error.

Warning: When the token comes from an untrusted source (e.g. user input or external requests), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected

token is the JsonWebToken string

secretOrPublicKey is a string (utf-8 encoded), buffer, or KeyObject containing either the secret for HMAC algorithms, or the PEM encoded public key for RSA and ECDSA. If jwt.verify is called asynchronous, secretOrPublicKey can be a function that should fetch the secret or public key. See below for a detailed example

As mentioned in this comment, there are other libraries that expect base64 encoded secrets (random bytes encoded using base64), if that is your case you can pass Buffer.from(secret, 'base64'), by doing this the secret will be decoded using base64 and the token verification will use the original random bytes.

options

  • algorithms: List of strings with the names of the allowed algorithms. For instance, ["HS256", "HS384"].

    If not specified a defaults will be used based on the type of key provided

    • secret - ['HS256', 'HS384', 'HS512']
    • rsa - ['RS256', 'RS384', 'RS512']
    • ec - ['ES256', 'ES384', 'ES512']
    • default - ['RS256', 'RS384', 'RS512']
  • audience: if you want to check audience (aud), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.

    Eg: "urn:foo", /urn:f[o]{2}/, [/urn:f[o]{2}/, "urn:bar"]

  • complete: return an object with the decoded { payload, header, signature } instead of only the usual content of the payload.
  • issuer (optional): string or array of strings of valid values for the iss field.
  • jwtid (optional): if you want to check JWT ID (jti), provide a string value here.
  • ignoreExpiration: if true do not validate the expiration of the token.
  • ignoreNotBefore...
  • subject: if you want to check subject (sub), provide a value here
  • clockTolerance: number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers
  • maxAge: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span vercel/ms.

    Eg: 1000, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms").

  • clockTimestamp: the time in seconds that should be used as the current time for all necessary comparisons.
  • nonce: if you want to check nonce claim, provide a string value here. It is used on Open ID for the ID Tokens. (Open ID implementation notes)
  • allowInvalidAsymmetricKeyTypes: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided.
// verify a token symmetric - synchronous
var decoded = jwt.verify(token, 'shhhhh');
console.log(decoded.foo) // bar

// verify a token symmetric
jwt.verify(token, 'shhhhh', function(err, decoded) {
  console.log(decoded.foo) // bar
});

// invalid token - synchronous
try {
  var decoded = jwt.verify(token, 'wrong-secret');
} catch(err) {
  // err
}

// invalid token
jwt.verify(token, 'wrong-secret', function(err, decoded) {
  // err
  // decoded undefined
});

// verify a token asymmetric
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, function(err, decoded) {
  console.log(decoded.foo) // bar
});

// verify audience
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo' }, function(err, decoded) {
  // if audience mismatch, err == invalid audience
});

// verify issuer
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer' }, function(err, decoded) {
  // if issuer mismatch, err == invalid issuer
});

// verify jwt id
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid' }, function(err, decoded) {
  // if jwt id mismatch, err == invalid jwt id
});

// verify subject
var cert = fs.readFileSync('public.pem');  // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer', jwtid: 'jwtid', subject: 'subject' }, function(err, decoded) {
  // if subject mismatch, err == invalid subject
});

// alg mismatch
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) {
  // if token alg != RS256,  err == invalid signature
});

// Verify using getKey callback
// Example uses https://github.com/auth0/node-jwks-rsa as a way to fetch the keys.
var jwksClient = require('jwks-rsa');
var client = jwksClient({
  jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
});
function getKey(header, callback){
  client.getSigningKey(header.kid, function(err, key) {
    var signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

jwt.verify(token, getKey, options, function(err, decoded) {
  console.log(decoded.foo) // bar
});

Need to peek into a JWT without verifying it? (Click to expand)

jwt.decode(token [, options])

(Synchronous) Returns the decoded payload without verifying if the signature is valid.

Warning: This will not verify whether the signature is valid. You should not use this for untrusted messages. You most likely want to use jwt.verify instead.

Warning: When the token comes from an untrusted source (e.g. user input or external request), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected

token is the JsonWebToken string

options:

  • json: force JSON.parse on the payload even if the header doesn't contain "typ":"JWT".
  • complete: return an object with the decoded payload and header.

Example

// get the decoded payload ignoring signature, no secretOrPrivateKey needed
var decoded = jwt.decode(token);

// get the decoded payload and header
var decoded = jwt.decode(token, {complete: true});
console.log(decoded.header);
console.log(decoded.payload)

Errors & Codes

Possible thrown errors during verification. Error is the first argument of the verification callback.

TokenExpiredError

Thrown error if the token is expired.

Error object:

  • name: 'TokenExpiredError'
  • message: 'jwt expired'
  • expiredAt: [ExpDate]
jwt.verify(token, 'shhhhh', function(err, decoded) {
  if (err) {
    /*
      err = {
        name: 'TokenExpiredError',
        message: 'jwt expired',
        expiredAt: 1408621000
      }
    */
  }
});

JsonWebTokenError

Error object:

  • name: 'JsonWebTokenError'
  • message:
    • 'invalid token' - the header or payload could not be parsed
    • 'jwt malformed' - the token does not have three components (delimited by a .)
    • 'jwt signature is required'
    • 'invalid signature'
    • 'jwt audience invalid. expected: [OPTIONS AUDIENCE]'
    • 'jwt issuer invalid. expected: [OPTIONS ISSUER]'
    • 'jwt id invalid. expected: [OPTIONS JWT ID]'
    • 'jwt subject invalid. expected: [OPTIONS SUBJECT]'
jwt.verify(token, 'shhhhh', function(err, decoded) {
  if (err) {
    /*
      err = {
        name: 'JsonWebTokenError',
        message: 'jwt malformed'
      }
    */
  }
});

NotBeforeError

Thrown if current time is before the nbf claim.

Error object:

  • name: 'NotBeforeError'
  • message: 'jwt not active'
  • date: 2018-10-04T16:10:44.000Z
jwt.verify(token, 'shhhhh', function(err, decoded) {
  if (err) {
    /*
      err = {
        name: 'NotBeforeError',
        message: 'jwt not active',
        date: 2018-10-04T16:10:44.000Z
      }
    */
  }
});

Algorithms supported

Array of supported algorithms. The following algorithms are currently supported.

alg Parameter ValueDigital Signature or MAC Algorithm
HS256HMAC using SHA-256 hash algorithm
HS384HMAC using SHA-384 hash algorithm
HS512HMAC using SHA-512 hash algorithm
RS256RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm
RS384RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm
RS512RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm
PS256RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0)
PS384RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0)
PS512RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0)
ES256ECDSA using P-256 curve and SHA-256 hash algorithm
ES384ECDSA using P-384 curve and SHA-384 hash algorithm
ES512ECDSA using P-521 curve and SHA-512 hash algorithm
noneNo digital signature or MAC value included

Refreshing JWTs

First of all, we recommend you to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system.

We are not comfortable including this as part of the library, however, you can take a look at this example to show how this could be accomplished. Apart from that example there are an issue and a pull request to get more knowledge about this topic.

TODO

  • X.509 certificate chain is not checked

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.