学习

如何在 API 网关缓存中使用 Redis

Will Johnston
作者
Will Johnston, Redis 开发者增长经理
Prasan Kumar
作者
Prasan Kumar, Redis 技术解决方案开发者
GITHUB 代码

以下是用于克隆本教程中使用的应用程序源代码的命令

git clone --branch v4.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions

什么是 API 网关缓存?#

您正在构建一个微服务应用程序。但您发现自己在处理身份验证方面遇到困难,既要能够重用代码,又要最大化性能。通常用于身份验证的方法包括会话、OAuth、授权令牌等。为了本教程的目的,我们假设使用授权令牌。在单体应用程序中,身份验证相当直接。

当请求传入时

  1. 1.解码 Authorization 头部。
  2. 2.验证凭据。
  3. 3.将会话信息存储在请求对象或缓存中,供应用程序后续使用。

然而,您可能对如何在微服务中实现这一点感到困惑。通常,在微服务应用程序中,API 网关充当客户端的单一入口点,负责将流量路由到相应的服务。根据请求的性质,这些服务可能需要或不需要用户进行身份验证。您可能认为在每个相应的服务中处理身份验证是个好主意。

虽然这可行,但会产生大量的重复代码。此外,由于您在每个服务中重复进行一些相同的工作,因此很难理解何时何地出现性能下降以及如何适当地扩展服务。处理身份验证更有效的方法是在 API 网关层进行处理,然后将会话信息向下传递给每个服务。

一旦您决定在 API 网关层处理身份验证,就必须决定将会话存储在哪里。

想象一下您正在构建一个使用 MongoDB/任何关系数据库作为主要数据存储的电子商务应用程序。您可以将会话存储在主数据库中,但请考虑应用程序需要多少次访问主数据库来检索会话信息。如果您有数百万客户,您不会希望对 API 的每个请求都去访问数据库。

这就是 Redis 的用武之地。

为什么应使用 Redis 进行 API 网关缓存#

Redis 是一个内存数据存储,除了其他用途外,它还是缓存会话数据的完美工具。Redis 可以减轻主数据库的负载,同时加快数据库读取速度。本教程的其余部分将介绍如何在电子商务应用程序的背景下实现这一点。

电子商务应用程序的微服务架构#

本教程其余部分讨论的电子商务微服务应用程序使用以下架构

  1. 1.产品服务:处理从数据库查询产品并将其返回给前端
  2. 2.订单服务:处理订单验证和创建
  3. 3.订单历史服务:处理查询客户订单历史
  4. 4.支付服务:处理订单支付
  5. 5.数字身份服务:处理数字身份存储和身份评分计算
  6. 6.API 网关:将服务统一到单一端点下
  7. 7.MongoDB/PostgreSQL:作为主数据库,存储订单、订单历史、产品等。
  8. 8.Redis:用作流处理器和缓存数据库
信息

在演示应用程序中,您不必使用 MongoDB/PostgreSQL 作为主数据库;您也可以使用其他 Prisma 支持的数据库。这只是一个示例。

该图说明了 API 网关如何使用 Redis 作为会话信息的缓存。API 网关从 Redis 获取会话,然后将其传递给每个微服务。这提供了一种在单个位置处理会话并将其贯穿其余微服务的简便方法。

提示

使用 Redis Cloud 集群 可获得线性扩展的优势,确保 API 调用在高峰负载下仍能正常运行。它还提供 99.999% 的正常运行时间和主动-主动地理分布,从而防止身份验证和会话数据丢失。

使用 Next.js 和 Tailwind 的电子商务应用程序前端#

电子商务微服务应用程序包含一个前端,使用 Next.js 和 TailwindCSS 构建。应用程序后端使用 Node.js。数据存储在 Redis 和 MongoDB/PostgreSQL 中,使用 Prisma。您可以在下面找到电子商务应用程序前端的截图

  • 仪表板:显示具有搜索功能的产品列表

购物车:将产品添加到购物车,然后使用“立即购买”按钮结账

GITHUB 代码

以下是用于克隆本教程中使用的应用程序源代码的命令

git clone --branch v4.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions

在微服务应用中使用 Redis 进行 API 网关缓存#

微服务架构的优点在于,每个服务都可以独立扩展。考虑到每个服务可能都需要身份验证,您可能希望为大多数请求获取会话信息。因此,使用 API 网关缓存和检索会话信息,然后将信息传递给每个服务,这是合理的。让我们看看如何实现这一点。

在我们的示例应用程序中,所有请求都通过 API 网关路由。我们使用 Express 设置 API 网关,并使用 Authorization 头部将授权令牌从前端传递给 API。对于每个请求,API 网关获取授权令牌并在 Redis 中查找。然后将其传递给正确的微服务。

此代码验证会话

import {
  createProxyMiddleware,
  responseInterceptor,
} from 'http-proxy-middleware';

//-----
const app: Express = express();

app.use(cors());
app.use(async (req, res, next) => {
  const authorizationHeader = req.header('Authorization');
  const sessionInfo = await getSessionInfo(authorizationHeader); //---- (1)

  //add session info to request
  if (sessionInfo?.sessionData && sessionInfo?.sessionId) {
    req.session = sessionInfo?.sessionData;
    req.sessionId = sessionInfo?.sessionId;
  }
  next();
});

app.use(
  '/orders',
  createProxyMiddleware({
    // https://:3000/orders/bar -> https://:3001/orders/bar
    target: 'https://:3001',
    changeOrigin: true,
    selfHandleResponse: true,
    onProxyReq(proxyReq, req, res) {
      // pass session info to microservice
      proxyReq.setHeader('x-session', req.session);
    },
    onProxyRes: applyAuthToResponse, //---- (2)
  }),
);

app.use(
  '/orderHistory',
  createProxyMiddleware({
    target: 'https://:3002',
    changeOrigin: true,
    selfHandleResponse: true,
    onProxyReq(proxyReq, req, res) {
      // pass session info to microservice
      proxyReq.setHeader('x-session', req.session);
    },
    onProxyRes: applyAuthToResponse, //---- (2)
  }),
);
//-----

const getSessionInfo = async (authHeader?: string) => {
  // (For demo purpose only) random userId and sessionId values are created for first time, then userId is fetched gainst that sessionId for future requests
  let sessionId = '';
  let sessionData: string | null = '';

  if (!!authHeader) {
    sessionId = authHeader.split(/\s/)[1];
  } else {
    sessionId = 'SES_' + randomUUID(); // generate random new sessionId
  }

  const nodeRedisClient = getNodeRedisClient();
  if (nodeRedisClient) {
    const exists = await nodeRedisClient.exists(sessionId);
    if (!exists) {
      await nodeRedisClient.set(
        sessionId,
        JSON.stringify({ userId: 'USR_' + randomUUID() }),
      ); // generate random new userId
    }
    sessionData = await nodeRedisClient.get(sessionId);
  }

  return {
    sessionId: sessionId,
    sessionData: sessionData,
  };
};

const applyAuthToResponse = responseInterceptor(
  // adding sessionId to the response so that frontend can store it for future requests

  async (responseBuffer, proxyRes, req, res) => {
    // detect json responses
    if (
      !!proxyRes.headers['content-type'] &&
      proxyRes.headers['content-type'].includes('application/json')
    ) {
      let data = JSON.parse(responseBuffer.toString('utf8'));

      // manipulate JSON data here
      if (!!(req as Request).sessionId) {
        data = Object.assign({}, data, { auth: (req as Request).sessionId });
      }

      // return manipulated JSON
      return JSON.stringify(data);
    }

    // return other content-types as-is
    return responseBuffer;
  },
);
信息

此示例并非旨在展示处理身份验证的最佳方法。相反,它说明了您在 Redis 方面可以做些什么。您可能会有不同的身份验证设置,但将会话存储在 Redis 中的概念是类似的。

在上面的代码中,我们检查 Authorization 头部,否则创建一个新的并将其存储在 Redis 中。然后我们从 Redis 中检索会话。稍后,在调用订单服务之前,我们将该会话附加到 x-session 头部。

现在让我们看看订单服务如何接收会话。

router.post(API_NAMES.CREATE_ORDER, async (req: Request, res: Response) => {
  const body = req.body;
  const result: IApiResponseBody = {
    data: null,
    error: null,
  };

  const sessionData = req.header('x-session');
  const userId = sessionData ? JSON.parse(sessionData).userId : "";
  ...
});

上面高亮显示的行展示了如何从 x-session 头部提取会话并获取 userId

准备好使用 Redis 进行 API 网关缓存了吗?#

就是这样!您现在知道如何在 API 网关缓存中使用 Redis 了。开始并不复杂,但这种简单的实践可以帮助您在构建微服务时实现扩展。

要了解更多关于 Redis 的信息,请查看下面的更多资源

更多资源#

使用 Redis 的微服务

通用