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);