【翻译-Node】利用NodeJS 和Express创建API网关(针对微服务)

2,066 阅读8分钟

【原文】: create-an-api-gateway-using-nodejs-and-express

【原作者】: Bram Janssen

假设您当前正在管理一堆 API 端点,例如大量微服务。也许在某些时候,您希望对路由到您的服务的请求有更多的控制,但您真的不想为每个微服务单独管理。这就是 API 网关可以发挥作用的地方。

API gateway

API 网关的目标是在客户端和您的微服务之间提供一个中间层。通过引入 API 网关,客户端将他们的请求发送到网关,网关将确保将请求重定向到相应的微服务。这样做时,API 网关可以对传入的请求执行额外的检查和验证,例如身份验证检查、指标收集、消息验证、响应转换、速率限制……

在这篇文章中,我想展示如何使用 NodeJS 创建一个非常基本的 API 网关。在开始实施之前,让我们先看一下我们的用例。在这篇文章中,我们将实现一个可以部署在市场基础设施中的 API 网关。此 API 网关的目标是检查传入请求并确保只有帐户中有足够信用的用户才能访问高级服务。如果不是这种情况,则应阻止执行该请求。鉴于这种情况,这将归结为以下要求:

  • 所有传入的请求都应重定向到其中一个微服务。
  • 一些路由只允许经过身份验证的请求,其他路由可以在没有有效身份验证的情况下访问(例如文档端点)。
  • 我们的免费使用服务需要速率限制,以减少后端的负载。
  • 高级服务要求用户在他们的帐户中有信用才能执行请求。

免责声明—— 这篇文章包含了一种使用 Express 创建 DYI API 网关的方法。在企业环境中,最好为此使用专用工具和软件。一些示例包是:

条件

在这篇文章的代码中,我们将使用 Keycloak ( www.keycloak.org/ ) 来支持用户授权。有许多关于如何设置 Keycloak 环境的在线资源。但是,这篇文章中的代码可以与任何支持与 NodeJS 集成的访问和身份工具一起使用。这意味着您只需替换代码的身份验证部分即可与您自己的系统兼容。

注意

文章中作者提到的利用Postman访问要求token的页面,需要额外阅读这篇文章进行构造

Accessing Keycloak Endpoints Using Postman

设置

首先,我们从设置项目和安装正确的依赖项开始。对于我们的 API 网关的基础,我们将使用 Express 服务器。因此,让我们从设置项目和安装 Express 开始。通过执行以下命令来做到这一点:

npm init 
npm install express --save

现在我们可以开始设置我们的基本 Express 服务器了。为什么不设置一个 hello world 端点来查看一切是否如我们预期的那样工作?作为任何受人尊敬的编码人员,我们需要从一个 hello world 示例开始……这意味着创建一个名为server.js 的文件并添加以下代码:

const express = require('express') 
​
const app = express() 
const port = 3000; 
​
​
app.get('/hello', (req, resp) => { 
    return resp.send('HELLO WORLD!'); 
}) 
​
app.listen(port, () => { 
    console.log(`Listen: http://localhost:${port}`) 
})

其实我们可以选择另一种办法:

express myProject --no-view

运行以下命令之一来试用我们的服务器:

node server.js
//or
npm run start

打开浏览器并导航到http://localhost:3000/hello和 TADA 🎉。你现在可以开始你的庆祝舞蹈了💃

dance

re

是时候加强它了!实施的下一步是向我们的服务器添加不同的功能,以创建我们的 API 网关。

日志

我们要添加的第一个功能是记录有关传入请求的信息。这可用于调试目的,也可用于收集有关 API 网关处理的请求的指标。

在我们的例子中,我们将使用morgan库 ( www.npmjs.com/package/mor… )。Morgan允许我们使用日志记录功能扩展我们的 Express 服务器。Morgan还支持 apache 之类的日志记录,它可以轻松地整合到现有的日志记录收集框架中,例如 ELK 堆栈。

首先在您现有的 NodeJS 项目中安装morgan :

npm install morgan --save

接下来,我们创建一个单独的logging.js文件来配置我们的日志记录设置并通过函数导出它们。

const morgan = require("morgan");
​
const setupLogging = (app) => {
    app.use(morgan('combined'));
}
​
exports.setupLogging = setupLogging

我们现在可以将我们的函数包含到现有的server.js中,以便启用请求日志记录:

const express = require('express')
const {setupLogging} = require("./logging");
​
​
const app = express()
const port = 3000;
​
​
setupLogging(app);
app.get('/hello', (req, resp) => {
    return resp.send('HELLO WORLD!');
})
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

如果我们现在重新启动服务器并刷新浏览器,我们可以看到请求已记录在服务器的控制台中。

log

路由配置

在继续集成其他功能之前,我们将首先创建我们希望在 API 网关中支持的不同路由的配置。根据应启用的功能,每个路由都可以具有多个属性。如何配置这些属性将在专门介绍不同功能的章节中解释。

为了创建配置,请创建一个包含以下内容的新routes.js文件:

const ROUTES = [
    {
        url: '/free',
        auth: false,
        creditCheck: true,
        rateLimit: {
            windowMs: 15 * 60 * 1000,
            max: 5
        },
        proxy: {
            target: "https://www.baidu.com",
            changeOrigin: true,
            pathRewrite: {
                [`^/free`]: '',
            },
        }
    },
    {
        url: '/premium',
        auth: true,
        creditCheck: true,
        proxy: {
            target: "https://www.baidu.com",
            changeOrigin: true,
            pathRewrite: {
                [`^/premium`]: '',
            },
        }
    },
    {
        url: '/credit',
        auth: false,
        creditCheck: true,
        rateLimit: {
            windowMs: 15 * 60 * 1000,
            max: 5
        },
        proxy: {
            target: "https://www.baidu.com",
            changeOrigin: true,
            pathRewrite: {
                [`^/free`]: '',
            },
        }
    },
]

module.exports = ROUTES

为简单起见,我们确定了两条(第三条为了测试后面的credit)路由,一条代表免费 (/free) 服务的端点,另一条代表高级 (/premium) 服务。每个服务可以具有以下属性:

  • url — 与传入请求匹配的 URL 路径。这可以是 Express 支持的任何路径。这意味着它还可以包含通配符来匹配多个路径。
  • auth — 布尔值,表示用户是否需要通过身份验证才能访问此端点。
  • creditCheck — 指示是否需要为此请求执行信用检查的布尔值。
  • rateLimit — 用于对服务应用速率限制的配置。
  • proxy — 代理配置,包含有关应将请求重定向到的目标的信息

Proxy

接下来,我们要设置应该应用于传入请求的代理规则。这是一个重要功能,因为我们的 API 网关将负责将传入请求重定向到实际的微服务。

我们可以使用一个名为http-proxy-middleware ( www.npmjs.com/package/htt… )的现有库来为我们的路由配置不同的代理规则。通过执行以下命令安装它:

npm install http-proxy-middleware --save

接下来,我们创建一个单独的proxy.js文件,该文件将为我们的路由创建代理。该文件的内容如下:

const { createProxyMiddleware } = require('http-proxy-middleware'); 
​
const setupProxies = (app, routes) => { 
    routes.forEach(r => { 
        app.use(r.url, createProxyMiddleware (r.proxy)); 
    }) 
} 
​
exports.setupProxies = setupProxies

您可能会注意到,这里没有发生很多事情。这个片段唯一做的就是为我们的配置中的每个路由添加createProxyMiddleware。这确实是所有人!我们唯一需要做的就是将正确的代理配置添加到我们的routes.js配置文件中。您可以使用文档 ( www.npmjs.com/package/htt… ) 查看不同的选项。如果您习惯使用 Angular、Apache 或 httpd 配置,您会发现许多相似之处。

剩下要做的就是将代理配置集成到我们的主服务器中。这可以通过将以下代码添加到我们的server.js文件中来完成:

const express = require('express')
​
const {ROUTES} = require("./routes");
​
const {setupLogging} = require("./logging");
const {setupProxies} = require("./proxy");
​
const app = express()
const port = 3000;
​
​
setupLogging(app);
setupProxies(app, ROUTES);
​
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

请注意,我们已经删除了我们的 hello world 端点。我们现在正在进入更高级的领域,所以不再需要一个 hello world 了😉

如果我们重新启动服务器,我们可以通过导航到配置中的 URL 来测试代码。在此示例中,您会注意到两者都会将您重定向到 Google 网站。您可以将routes.js更改为代理到任何主机和端口组合,并将其与一些花哨的路径重写结合起来。

认证

是时候向我们的 API 网关添加身份验证了。我们配置中的某些路由需要在请求中包含有效的用户身份验证,以便进一步执行。如先决条件所述,我们将依靠 Keycloak 来验证传入请求的访问令牌。

Keycloak 已经通过keycloak-connect ( www.npmjs.com/package/key… ) 模块支持 Express 集成。让我们从安装所需的模块开始:

npm install keycloak-connect express-session --save

现在我们创建一个名为auth.js的单独文件,以配置我们的身份验证并将不同的规则应用于我们的配置路由:

const Keycloak = require('keycloak-connect');
const session = require('express-session');
​
const setupAuth = (app, routes) => {
    var memoryStore = new session.MemoryStore();
    var keycloak = new Keycloak({ store: memoryStore });
​
    app.use(session({
        secret:'<RANDOM GENERATED TOKEN>',
        resave: false,
        saveUninitialized: true,
        store: memoryStore
    }));
​
    app.use(keycloak.middleware());
​
    routes.forEach(r => {
        if (r.auth) {
            app.use(r.url, keycloak.protect(), function (req, res, next) {
                next();
            });
        }
    });
}
​
exports.setupAuth = setupAuth

在我们的文件中,我们定义了一个名为setupAuth的函数,它将快速应用程序和我们的路由配置作为输入参数。为了启用 Keycloak 集成,我们需要创建一个新的内存存储,设置应用程序以使用会话并激活 Keycloak 中间件。接下来,我们可以通过以下代码段保护我们的端点:

app.use(r.url, keycloak.protect(), function (req, res, next) { 
      next(); 
});

此代码在指定的 URL 上添加 Keycloak 中间件 ( keycloak.protect() )。 添加了一个额外的回调函数,使我们能够向代码添加额外的日志记录。在这种情况下,我们只需调用next() 函数,它告诉 Express 继续处理请求。

为了使 Keycloak 集成工作,我们还需要一个包含一些 Keycloak 特定信息的附加配置文件。有关设置的更多信息,请参阅官方 Keycloak 文档。以下是本项目中使用的示例keycloak.json配置:

{
    "realm": "<REALM>",
    "bearer-only": true,
    "auth-server-url": "<AUTH_URL>",
    "ssl-required": "external",
    "resource": "<CLIENT>",
    "confidential-port": 0
}

下一步是将身份验证设置包含到我们的主服务器中。我们可以通过使用以下代码更新我们的server.js文件来做到这一点:

const express = require('express')
​
const {ROUTES} = require("./routes");
​
const {setupLogging} = require("./logging");
const {setupProxies} = require("./proxy");
const {setupAuth} = require("./auth");
​
const app = express()
const port = 3000;
​
​
setupLogging(app);
setupAuth(app, ROUTES);
setupProxies(app, ROUTES);
​
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})
​

重新启动服务器后,您会注意到导航到高级 url ( http://localhost:3000/premium ) 现在将导致“拒绝访问”页面。但是,仍然可以访问免费 url ( http://localhost:3000/free ) 👍。您可以使用 Postman 等工具将访问令牌添加到您的请求中,以检查 Keycloak 集成是否正常工作。在这种情况下,Postman 还应该在对高级 URL 执行 GET 请求时显示 Google 网站。

navigate

Rate Limit

我们实施的下一阶段将为我们的网关端点添加速率限制。在我们的示例中,我们将对免费服务应用速率限制。降低某些服务的吞吐量可以具有多种优势,例如减少某些微服务上的负载并更受控制,激励用户使用溢价来消除吞吐量限制,......

就像我们已经实现的其他功能一样,已经有一个 NPM 库,称为express-rate-limit ( www.npmjs.com/package/exp… ),可用于将速率限制集成到现有的 Express 服务器。可以通过执行以下命令来安装它:

npm install --save express-rate-limit

安装后,我们通过创建一个单独的ratelimit.js文件来继续我们的开发,其中包含以下内容:

const rateLimit = require("express-rate-limit");
​
const setupRateLimit = (app, routes) => {
    routes.forEach(r => {
        if (r.rateLimit) {
            app.use(r.url, rateLimit(r.rateLimit));
        }
    })
}
​
module.exports = setupRateLimit
​

您会再次注意到它看起来与其他功能的实现非常相似。上面的代码片段循环遍历所有路由,并在适用于给定 url 的情况下添加速率限制中间件。提供给速率限制中间件的设置可以在模块的文档(www.npmjs.com/package/exp…)中找到,并且可以直接从路由中的路由配置中读取。 .js文件。

如果我们查看routes.js配置,我们可以看到我们确实通过指定以下属性对免费路由应用了速率限制:

rateLimit: {
  windowMs: 15 * 60 * 1000,
    max: 5
},

这些设置限制请求 端点每 15 分钟最多 5 个请求。

接下来,我们需要在创建服务器时激活速率限制设置。这可以通过使用以下内容扩展server.js文件来完成:

const express = require('express')
​
const {ROUTES} = require("./routes");
​
const {setupLogging} = require("./logging");
const {setupRateLimit} = require("./ratelimit");
const {setupProxies} = require("./proxy");
const {setupAuth} = require("./auth");
​
const app = express()
const port = 3000;
​
​
setupLogging(app);
setupRateLimit(app, ROUTES);
setupAuth(app, ROUTES);
setupProxies(app, ROUTES);
​
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

所以现在让我们测试我们的解决方案。重新启动服务器并转到http://localhost:3000/free的免费端点。什么都没有改变,对吧?现在刷新页面几次。刷新大约 5 次后,您会看到一条错误消息:“请求过多,请重试”。现在,我们通过应用速率限制成功地限制了服务的吞吐量。

limit

信用检查

我们要添加的最后一件事是对高级端点请求的信用检查。如果用户在他/她的帐户中没有足够的信用来执行请求,则应该阻止它。

然而,在这篇文章中,我们将创建一个更通用的信用检查示例实现。本章的目标是创建任何类型的中间件,您希望在重定向或阻止请求之前对其进行额外检查。

正如您在前面的段落中可能已经注意到的那样,我们一直使用 Express 中间件来将它们组合到请求评估/执行链中。Express 允许我们创建自定义中间件并将其添加到验证链中。每个中间件都定义为一个函数:

function(request, response, next) {
    // Add custom code here
}

在中间件函数中,您可以访问请求响应对象以及名为next的函数。在中间件执行期间,您有多种选择:

  • 通过响应对象向客户端发送响应,结束请求的处理,例如:
res.status(500).send({error});
  • 完成中间件并继续执行链中的下一个中间件。这就是next() 函数的用武之地。它告诉 Express 你的中间件没有遇到任何错误,它可以通过下一个中间件继续评估请求。

所以现在让我们把它付诸实践。在我们的示例中,我们希望对每个传入请求进行信用检查。我们首先创建一个函数,该函数将在一个名为creditcheck.js 的文件中执行实际的信用检查。

const checkCredit = (request) => { 
    return new Promise((resolve, reject) => { 
           // 自定义代码
           if (ok) { 
              resolve(); 
           } else { 
              reject('No credits'); 
           }
    }) 
}

在上面的示例中,我们省略了信用检查的实际代码实现,因为它可以替换为您希望根据请求对象进行额外检查的任何自定义代码。但是,为了测试代码,我们可以编写一些会导致 500 毫秒延迟的负面信用检查的代码,模拟附加请求的执行:

const checkCredit = (req) => { 
    return new Promise((resolve, reject) => { 
        console.log("Checking credit with token", req.headers["authorization"]); 
        setTimeout (() => { 
            reject ('None'); 
        }, 500); 
    }) 
}
​

正如您在上面的代码中看到的那样,我们将拒绝承诺,这表示用户的错误或负信用余额。可以通过对另一个实际返回积分数量的微服务执行任何附加请求来轻松替换此代码。

我们的下一步是将creditCheck函数添加为中间件,以处理我们的传入请求:

const checkCredit = (req) => {
    return new Promise((resolve, reject) => {
        console.log("Checking credit with token", req.headers["authorization"]);
        setTimeout(() => {
            reject('No sufficient credits');
        }, 500);
    })
}
​
const setupCreditCheck = (app, routes) => {
    routes.forEach(r => {
        if (r.creditCheck) {
            app.use(r.url, function(req, res, next) {
                checkCredit(req).then(() => {
                    next();
                }).catch((error) => {
                    res.status(402).send({error});
                })
            });
        }
    })
}
​
module.exports = setupCreditCheck
​

在这段代码中,我们执行creditCheck函数并等待其结果。如果解析正确,我们通过调用next() 函数通知 Express 中间件已成功执行。如果信用检查被拒绝,我们将停止处理请求并将响应发送回客户端。

我们实现的最后一步是将信用检查配置添加到服务器的启动脚本中,从而生成我们的最终server.js文件:

const express = require('express')
​
const {ROUTES} = require("./routes");
​
const {setupLogging} = require("./logging");
const {setupRateLimit} = require("./ratelimit");
const {setupCreditCheck} = require("./creditcheck");
const {setupProxies} = require("./proxy");
const {setupAuth} = require("./auth");
​
const app = express()
const port = 3000;
​
​
setupLogging(app);
setupRateLimit(app, ROUTES);
setupAuth(app, ROUTES);
setupCreditCheck(app, ROUTES);
setupProxies(app, ROUTES);
​
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

剩下要做的就是重新启动服务器,信用检查将应用于您的routes.js配置中的相关路由。您也可以通过启用免费路线轻松测试信用检查。如果您重新启动服务器并导航到免费 url ( http://localhost:3000/free ),您将收到一条消息,提示您没有足够的积分 👍。

请记住,我们还对这条路线应用了速率限制,因此您可能太快而不得不等待 15 分钟或一起禁用速率限制。

limit rate

做得好!我们已经创建了我们的自定义 API 网关实现。您还可以在 Github ( github.com/JanssenBrm/… ) 上找到完整代码。我希望这篇文章能让您更好地了解如何在 Express 中使用中间件,以及为您的 API 网关添加更多有用功能的灵感。继续编码!