软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

NodeJS微服务间传递用户上下文

后端包含众多微服务时,如果每个微服务都是自己去查询用户信息(比如: 用户ID、登录名),会造成用户微服务压力过大、响应延时增大。为了解决这类问题,可以在网关服务中查询用户信息等共用信息,然后传递到其它的微服务中,微服务之间也相互传递。

为了前端代码复用,在NodeJS中也使用Axios完成网络请求,也方便我们拦截处理。文本演示Express + Axios + Async hooks解决微服务间用户上下文传递的问题。

1. Express初始化用户上下文

使用Express中间件机制查询用户信息,然后存储到store中,key为当前executionAsyncId()。

const app = require('express')();

function userContextHandler(req, res, next) {
  const userCtx = {
    userId: 1000, // TODO: query user info from session
    userName: 'userXXX',
    reqHeaders: {
    },
  };

  // 'host' 用在反向代理; 'user-agent'
  ['language', 'session-id'].forEach((key) => {
    if (req.headers[key]) {
      userCtx.reqHeaders[key] = req.headers[key];
    }
  });

  userCtx.reqHeaders["user-id"] = `${userCtx.userId}`;
  userCtx.reqHeaders["user-name"] = userCtx.userName;

  store.set(asyncHooks.executionAsyncId(), userCtx);
  next();
}

app.use(userContextHandler);

2. Async hooks存储用户上下文

每次初始化异步资源时,务必从triggerAsyncId中继承用户上下文。

const asyncHooks = require('async_hooks');

const store = new Map();

const asyncHook = asyncHooks.createHook({
  init: (asyncId, _, triggerAsyncId) => {
    if (store.has(triggerAsyncId)) {
      store.set(asyncId, store.get(triggerAsyncId))
    }
  },
  destroy: (asyncId) => {
    if (store.has(asyncId)) {
      store.delete(asyncId);
    }
  }
});

asyncHook.enable();

3. Axios拦截设置用户上下文

请求发送之前,需要把用户上下文放到http头部中。

axios.interceptors.request.use((config) => {
  const asyncId = asyncHooks.executionAsyncId();

  if (!store.has(asyncId)) {
    return config;
  }

  const userCtx = store.get(asyncId);

  if (!config.headers) {
    config.headers = userCtx.reqHeaders;
  } else {
    Object.keys(userCtx.reqHeaders).forEach((key) => {
      if (!hasProperty(config.headers, key)) {
        config.headers[key] = userCtx.reqHeaders[key];
      }
    });
  }

  return config;
}

4. 完整代码

const axios = require('axios').default;
const http = require('http');
const app = require('express')();
const asyncHooks = require('async_hooks');

const store = new Map();

const asyncHook = asyncHooks.createHook({
  init: (asyncId, _, triggerAsyncId) => {
    if (store.has(triggerAsyncId)) {
      store.set(asyncId, store.get(triggerAsyncId))
    }
  },
  destroy: (asyncId) => {
    if (store.has(asyncId)) {
      store.delete(asyncId);
    }
  }
});

asyncHook.enable();

function hasProperty(obj, prop) {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

axios.interceptors.request.use((config) => {
  const asyncId = asyncHooks.executionAsyncId();

  if (!store.has(asyncId)) {
    return config;
  }

  const userCtx = store.get(asyncId);

  if (!config.headers) {
    config.headers = userCtx.reqHeaders;
  } else {
    Object.keys(userCtx.reqHeaders).forEach((key) => {
      if (!hasProperty(config.headers, key)) {
        config.headers[key] = userCtx.reqHeaders[key];
      }
    });
  }

  return config;
})

/**
 *
 * @param {http.IncomingMessage} req
 * @param {http.ServerResponse} res
 * @param {*} next
 */
function userContextHandler(req, res, next) {
  const userCtx = {
    userId: 1000, // TODO: query user info from session
    userName: 'userXXX',
    reqHeaders: {
    },
  };

  // 'host' 用在反向代理; 'user-agent'
  ['language', 'session-id'].forEach((key) => {
    if (req.headers[key]) {
      userCtx.reqHeaders[key] = req.headers[key];
    }
  });

  userCtx.reqHeaders["user-id"] = `${userCtx.userId}`;
  userCtx.reqHeaders["user-name"] = userCtx.userName;

  store.set(asyncHooks.executionAsyncId(), userCtx);
  next();
}

function errorHandler (err, req, res, next) {
  res.status(500);
  res.render('error', { error: err });
}

app.use(userContextHandler);
app.use(errorHandler);

app.get('/', async function (req, res, next) {
  try {
    const resAll = await Promise.all([
      axios.get('http://apm-nodejs-app-b:8080/api/hello-a-b'),
      axios.get('http://apm-nodejs-app-c:8080/api/hello-a-c'),
    ]);

    res.send({
      a: 'Hello World!',
      b: resAll[0].data,
      c: resAll[1].data,
    });
  }
  catch (err) {
    next(err);
  }
});

app.listen(8080);