如何在处理用Express开发的REST应用程序时定制错误响应

318 阅读10分钟

HTTP错误状态代码简介

HTTP状态码是服务器响应客户的请求而返回的一个代码号。许多可用的HTTP状态代码可以根据它们的编号分为以下五类:

正如你所看到的,只有两类HTTP状态代码代表错误。 4xx5xx.

4xx类HTTP状态码指的是由于客户端的请求造成的错误,例如,由于请求格式不正确。当涉及到 4xx错误时,服务器应该向客户提供错误背后的额外信息,以防止再次造成错误。

5xx类的HTTP状态代码指的是服务器在处理请求时遇到的错误。出于安全考虑,你不应该向客户端提供有关这类错误的额外信息。这是因为你不希望攻击者根据你返回给他们的信息来了解服务器的工作情况。

使用正确的HTTP错误状态代码对于帮助客户端了解发生了什么很重要。事实上,每个 4xx5xxHTTP状态码都对应着一种特定类型的错误。例如 400状态码应该在客户端请求的形式与API预期的不一致时使用,而 401状态码应该在客户端没有提供证书或无效证书时返回。然而,对于 4xx错误,仅有HTTP状态码可能是不够的,你应该向客户提供更多的信息。

让我们深入研究一下为什么你可能需要定制你的HTTP错误响应。

为什么要定制错误响应?

考虑到 4xx错误是由客户的请求引起的,没有收到足够的信息来理解为什么该请求会失败,这可能是令人沮丧的。因此,你需要定制错误响应,为客户提供关于所发生问题的更多细节。这意味着在返回HTTP错误状态代码的同时,还要返回额外的数据。

例如,想象一个情况,一个API需要一个数字customerId 参数。现在,一个客户在没有customerId 参数的情况下调用该API,或者使用一个非数字字符串。结果是,它将从服务器上收到一个普通的 400 Bad Request错误。你可以想象,默认的 "Bad Request "消息不能帮助调用者理解他们应该如何调用该API。另一方面,如果服务器返回一个 400HTTP状态代码响应,包含 "customerId required "或 "customerId must be a number "信息,客户就可以弄清楚他们需要如何正确地调用API,并避免两次犯同样的错误。因此,提供一个详细的关于 4xx帮助最终用户,这也是一些拥有公共API的公司使用自定义错误响应的原因:

请注意,定制错误响应是一个安全问题。尽管在发生错误时提供额外的信息可能是有帮助的,但你不应该返回可能危及你的应用程序安全的信息。这就是为什么你应该考虑在错误响应上进行定制,只在 4xx错误,这些错误都是可以直接指向客户端的。相反,你不应该在你的 5xx错误响应上提供额外的信息。

现在,让我们看看自定义错误响应的有趣格式。

错误响应的格式

为了了解如何定义一个好的自定义错误响应格式,你可以看看像谷歌、苹果和亚马逊这样拥有公共API的知名公司在出现错误时的做法。例如,GitHub在出现以下错误时的回报是这样的 401错误时的返回:

{
   "message": "Requires authentication",
   "documentation_url": "https://docs.github.com/rest/reference/repos#create-an-organization-repository"
}

返回一个包含错误描述的字符串信息是一种常见的做法,但特别有见地的是documentation_url 领域。这是一个绝妙的想法,为调用者提供了解决方案的途径,而不仅仅是报告错误信息。另外,如果有什么变化,你不必更新你的错误响应。将要改变的是错误响应中使用的文档页面的内容。此外,根据定义,返回文档的公共链接并不构成安全问题,因为任何人都已经可以访问该文档。

现在,让我们看看如何在Express服务器中定制你的错误响应以遵循这种格式。

在Express中自定义错误响应

在这里,你将学习如何通过扩展Auth0 Hello World API来定制401 Unauthorized403 Forbidden HTTP错误。这是一个很好的例子,可以从JavaScript中的演示Express应用程序开始。另外,HTTP 401403错误属于 4xx类,是可能需要自定义响应的错误的好例子。

现在让我们深入了解一下Auth0 Express Hello World API。

开始使用Auth0 Hello Word API

你可以在这里找到Auth0 Express Hello World API repo。详细来说,让我们关注一下 basic-role-based-access-control分支。你可以用下面的命令克隆它,看一下它的代码:

git clone https://github.com/auth0-developer-hub/api_express_javascript_hello-world.git --branch basic-role-based-access-control

这个代码库包含了Express.js Hello World API,通过Auth0提供的角色验证进行保护。你需要一个Auth0的账户来配置这个应用程序,如果你没有,现在就免费注册吧!

注意,这种RBAC(基于角色的访问控制)策略依赖于Auth0的用户管理功能。你可以在这里了解更多关于带有基本RBAC保护的Express Hello World API。

具体来说,认证和授权的错误逻辑是在 error.middleware.js文件中处理,如下所示:

// src/middleware/error.middleware.js

const errorHandler = (error, request, response, next) => {
  if (error.status === 401 && error.message === "Unauthorized") {
    const status = 401;
    const message = "Requires authentication";

    response.status(status).json({ message });

    return;
  }

  if (
    error.status === 401 &&
    error.code === "invalid_token" &&
    error.message === "Permission denied"
  ) {
    const status = 403;
    const message = error.message;

    response.status(status).json({ message });

    return;
  }

  const status = error.statusCode || error.code || 500;
  const message = error.message || "internal error";

  response.status(status).json({ message });
};

module.exports = {
  errorHandler,
};

这里,由Auth0 SDK引发的 "未授权" 401错误被拦截并转化为以下内容 401HTTP状态代码错误响应:

"Requires authentication"

同样地,由Auth0 SDK产生的 "权限拒绝 "的 401同样,由Auth0 SDK产生的 "权限拒绝 "错误也被转换为以下内容 403HTTP状态代码错误响应:

"Permission denied"

请注意,用于提供授权和认证的Auth0 Express库不会在访问令牌没有得到授权时返回一个 403注意,当访问令牌没有与具有预期角色的用户相关联时,不会像你所期望的那样返回错误。这是因为Auth0 SDK团队希望尽可能地保持库的通用性,所以总是返回一个通用的 401错误。这取决于你的权衡,决定SDK返回的错误是否最适合你的使用情况。在这里,它不是,这就是为什么 401错误变成了 403错误。

现在让我们来了解一下所使用的Auth0库的情况。

如何检查访问权限?

Express Hello World API依赖于 express-oauth2-jwt-bearerAuth0库来实现授权和认证。如果你对它不熟悉的话。 express-oauth2-jwt-bearer是Express的一个认证中间件,可以验证JWT格式的承载访问令牌。

详细来说。 express-oauth2-jwt-bearer文件中使用了 src/middleware/auth0.middleware.js文件中使用,具体如下:

// src/middleware/auth0.middleware.js

const { auth, claimCheck } = require("express-oauth2-jwt-bearer");
const dotenv = require("dotenv");

dotenv.config();

const validateAccessToken = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

const checkRequiredPermissions = (requiredPermissions) => {
  return (req, res, next) => {
    const permissionCheck = claimCheck((payload) => {
      const permissions = payload.permissions || [];

      return requiredPermissions.every((requiredPermission) =>
        permissions.includes(requiredPermission)
      );
    }, "Permission denied");

    permissionCheck(req, res, next);
  };
};

module.exports = {
  validateAccessToken,
  checkRequiredPermissions,
};

validateAccessToken()函数是由 express-oauth2-jwt-bearerauth()函数,它接收一个 AuthOptions对象并返回一个中间件函数,该函数可用于将一个Express端点限制给授权用户。同样地,该 checkRequiredPermissions()函数是由 express-oauth2-jwt-bearerclaimCheck()函数,它接收一个 JWTPayload对象并返回一个中间件函数,该函数可用于将一个Express端点限制为已认证的用户。

这两个函数可以用来提供认证和授权,具体如下:

// src/services/example.js

const express = require("express");
const {
  checkRequiredPermissions,
  validateAccessToken,
} = require("../middleware/auth0.middleware.js");

const messagesRouter = express.Router();

// a public endpoint
messagesRouter.get("/public", (req, res) => {
  res.status(200).json("Hello, World!");
});

// an Express endpoint accessible only by authenticated users
// thanks to the validateAccessToken middleware
messagesRouter.get("/protected", validateAccessToken, (req, res) => {
  res.status(200).json("Hello, World!");
});

// an Express endpoint accessible only by authenticated and authorized users
// thanks to the validateAccessToken and checkRequiredPermissions middleware
// functions respectively
messagesRouter.get(
  "/admin",
  validateAccessToken,
  checkRequiredPermissions(["read:admin-messages"]),
  (req, res) => {
    const message = getAdminMessage();

    res.status(200).json(message);
  }
);

module.exports = { messagesRouter };

定制 401403错误响应

现在让我们来看看如何扩展 basic-role-based-access-control分支应用程序,以实现对以下内容的自定义错误响应 401403错误。你可以在支持这篇文章的GitHub仓库中找到完整的代码。要实现这个目标,只需修改文件 error.middleware.js如下所示:

// src/middleware/error.middleware.js

const errorHandler = (error, request, response, next) => {
  if (error.status === 401 && error.message === "Unauthorized") {
    // defining the HTTP status code
    const status = 401;
    // standard HTTP 401 error message
    const message = "Unauthorized";
    // the link to the hosted version of the "how-to-handle-authentication" HTML page
    // you can find in the /docs folder
    const authority = `${request.protocol}://${request.hostname}:${process.env.PORT}`;
    const documentationLink = `${authority}/docs/how-to-handle-authentication.html`;

    // implementing a custom error response on 401 errors
    // matching the GitHub error response format
    response.status(status).json({
      message: message,
      documentationLink: documentationLink
    });

    return;
  }

  if (
      error.status === 401 &&
      error.code === "invalid_token" &&
      error.message === "Permission denied"
  ) {
    // defining the HTTP status code
    const status = 403;
    // standard HTTP 403 error message
    const message = "Forbidden";
    // the link to the hosted version of the "how-to-handle-authorization" HTML page
    // you can find in the /docs folder
    const authority = `${request.protocol}://${request.hostname}:${process.env.PORT}`;
    const documentationLink = `${authority}/docs/how-to-handle-authorization.html`;

    // implementing a custom error response on 403 errors
    // matching the GitHub error response format
    response.status(status).json({
      message: message,
      documentationLink: documentationLink
    });

    return;
  }

  const status = error.statusCode || error.code || 500;
  const message = error.message || "internal error";

  response.status(status).json({ message });
};

module.exports = {
  errorHandler,
};

这就是 401错误响应的样子。

{
   "message": "Unauthorized",
   "documentationLink": "https://your-domain.com/docs/how-to-handle-authentication.html"
}

而这是 403错误响应的样子:

{
   "message": "Forbidden",
   "documentationLink": "https://your-domain.com/docs/how-to-handle-authorization.html"
}

请注意,在这两种情况下,错误信息都与标准的HTTP错误信息一致。另外,错误响应中使用的链接指向两个自定义静态HTML页面的托管版本,你可以在项目的 /docs文件夹中找到。

The "How To Handle Authentication" sample doc page

请记住,这些HTML文档只是带有lorem ipsum文本的样本页。因此,你应该用完整的信息填充这些页面,以处理认证和授权错误,或者用适当的链接替换你的文档的URL。

测试 401403错误响应

首先,克隆扩展版的 basic-role-based-access-controlAuth0 Hello World API分支,使用以下命令:

git clone https://github.com/auth0-blog/extended-basic-role-based-access-control

然后,用以下命令进入 extended-basic-role-based-access-control项目目录,用这个命令:

cd extended-basic-role-based-access-control

现在,用安装npm项目的依赖项:

npm install

然后,在项目根目录下创建一个 .env文件,并按如下方式填充:

PORT=6060
CLIENT_ORIGIN_URL=http://localhost:4040
AUTH0_AUDIENCE=<YOUR-AUTH0-AUDIENCE>
AUTH0_DOMAIN=<YOUR-AUTH0-DOMAIN>

替换为 <YOUR-AUTH0-AUDIENCE><YOUR-AUTH0-DOMAIN>用你在Auth0仪表板上找到的值替换。此外,你还需要检索一个有效的 <AUTH0-ACCESS-TOKEN>值。按照官方Auth0文档的这个指南,学习如何检索这些值。

用下面的命令启动Express.js API服务器:

npm run dev

你现在可以开始测试401错误响应了。例如,让我们尝试调用 api/messages/protected需要认证的API,但没有必要的Auth0访问令牌:

curl --request GET \
  --url http:/localhost:6060/api/messages/protected

你会收到一个包含以下JSON的401错误响应:

{
   "message": "Unauthorized",
   "documentationLink": "https://your-domain.com/docs/how-to-handle-authentication.html"
}

相反,让我们尝试使用有效的 <AUTH0-ACCESS-TOKEN>检索:

curl --request GET \
  --url http:/localhost:6060/api/messages/protected \
  --header 'authorization: <AUTH0-ACCESS-TOKEN>'

在这种情况下,你会得到:

{
    text: "This is a protected message."
}

复制一个 403错误需要更多的努力。这是因为你需要运行一个使用Auth0 SDK的前端客户端应用程序。按照官方文档中的这个教程来学习如何在Auth0中设置RBAC。确保设置一个 "read:admin-messages "角色,并将其与管理员用户角色关联。

然后,使用列表中的任何一个Auth0前端客户端演示,你可以在这里找到登录和访问 GET /api/messages/admin端点,该端点由先前启动的Express服务器在6060端口上暴露。

当用授权的管理员用户登录时,你会得到:

{
    "text": "This is an admin message."
}

另一方面,当使用一个未经授权的用户时,你将收到以下JSON错误响应:

{
   "message": "Forbidden",
   "documentationLink": "https://your-domain.com/docs/how-to-handle-authorization.html"
}

通过对一些最臭名昭著的威胁的实践探索来学习网络安全。

总结

在这篇文章中,我们研究了如何在处理用Express开发的REST应用程序时定制错误响应。这只需要几行代码,但你不应该轻视它。相反,在实施时应该考虑到安全问题,以避免向攻击者提供关于你的后端如何工作的有用信息。首先,我们介绍了HTTP错误状态代码,并澄清了为什么你可能需要在 4xx错误。然后,我们研究了拥有公共API的公司如何处理 4xx来定义一个真实世界中的错误响应格式。最后,我们学习了在涉及到Auth0 Express Hello World API返回的401和403错误时,如何实现这种格式,使用 express-oauth2-jwt-bearer库进行认证和授权时,如何实现这种格式。

谢谢你的阅读!