Express Gateway授权:服务间调用loggedUser传递方案咨询
微服务间用户身份传递的最佳方案
问题背景
我的架构是网关统一处理所有请求,验证JWT Token后将loggedUser添加到请求体中,再转发到对应的业务容器。现在遇到两个核心问题:
- 容器之间互相调用时,
loggedUser仅存在于第一个接收请求的容器,无法传递到后续调用的其他容器; - 尝试给每个容器添加拦截器自动携带
loggedUser,不仅工作量大,还出现了请求残留旧loggedUser值的异常(未登录用户请求时显示上一个用户的信息)。
当前相关代码片段
路由配置
router.get('/load/account', expressRouterAdapter(loadAccountControllerFactory()))
Express路由适配器
export const expressRouterAdapter = (controller: Controller): any => { return async (req: Request & { loggedUser: any}, res: Response, next: NextFunction) => { const { body, params, headers, query } = req const interceptor = InterceptorAdapter.interceptRequest() interceptor.addHeader({Authorization: headers.authorization}) const response = await controller.handle({ body, params, headers, query }) res.status(response.statusCode) if (response.statusCode === 200) { res.json(response.body) } else { res.json({ error: response.body }) } }
请求拦截器
export class InterceptorAdapter implements AddRequestHeader { static request: AxiosInterceptorManager<AxiosRequestConfig> private constructor( expressRequest?: AxiosInterceptorManager<AxiosRequestConfig> ) { InterceptorAdapter.request = expressRequest } static interceptRequest(): InterceptorAdapter { axios.interceptors.request.clear() return new InterceptorAdapter(axios.interceptors.request) } addHeader(header: AddRequestHeader.Header) { InterceptorAdapter.request.use(req => { return { ...req, headers: { ...header } } }, error => error) } }
网关策略
- cors: - log: action: message: 'auth ${req.method}' - jwt: action: secretOrPublicKey: 'MY KEY' checkCredentialExistence: false - request-transformer: action: body: add: loggedUser: req.user - proxy: - action: serviceEndpoint: svcap20Service changeOrigin: true
问题根源分析
- 拦截器设计缺陷:当前
InterceptorAdapter使用静态属性存储请求拦截器,且每次调用interceptRequest()都会清空全局axios拦截器,多请求并发时,不同请求的Authorization头会互相覆盖,导致旧值残留; - 身份传递方式错误:网关把
loggedUser放在请求体中,而微服务间调用通常用HTTP头传递身份信息,请求体仅用于业务数据,这直接导致跨服务调用时无法自动携带身份。
最佳解决方案
1. 统一用HTTP头传递身份信息
网关不要把loggedUser放到请求体,而是将JWT Token直接透传给下游服务,或者把loggedUser的核心信息(如用户ID、权限)放到自定义HTTP头中,修改网关的request-transformer策略:
- request-transformer: action: headers: add: # 方案1:透传原始Token(推荐,下游服务可自行验证解析) Authorization: ${req.headers.authorization} # 方案2:传递序列化后的用户信息(减少下游解析成本) X-Logged-User: ${req.user | toJson}
这样所有下游服务都能通过HTTP头获取用户身份,跨服务调用时只需要携带这些头即可。
2. 重构拦截器,避免全局状态污染
当前拦截器的静态属性是并发问题的元凶,改成请求级别的拦截器,为每个请求创建独立的axios实例,避免全局状态覆盖:
// 重构后的InterceptorAdapter,去掉静态属性 export class InterceptorAdapter implements AddRequestHeader { private readonly requestInterceptor: AxiosInterceptorManager<AxiosRequestConfig> private constructor(axiosInstance: AxiosInstance) { this.requestInterceptor = axiosInstance.interceptors.request } // 为每个请求创建专属axios实例和拦截器 static createForRequest(headers: Record<string, string>): AxiosInstance { const axiosInstance = axios.create() const adapter = new InterceptorAdapter(axiosInstance) adapter.addHeader(headers) return axiosInstance } addHeader(header: AddRequestHeader.Header) { this.requestInterceptor.use(req => { // 合并原有头和新头,避免覆盖 return { ...req, headers: { ...req.headers, ...header } } }, error => Promise.reject(error)) } }
然后在路由适配器中使用:
export const expressRouterAdapter = (controller: Controller): any => { return async (req: Request & { loggedUser: any}, res: Response, next: NextFunction) => { const { body, params, headers, query } = req // 为当前请求创建专属axios实例,携带身份头 const axiosInstance = InterceptorAdapter.createForRequest({ Authorization: headers.authorization, 'X-Logged-User': headers['x-logged-user'] }) // 把axios实例传给controller,用于跨服务调用 const response = await controller.handle({ body, params, headers, query, httpClient: axiosInstance }) res.status(response.statusCode) res.json(response.statusCode === 200 ? response.body : { error: response.body }) } }
3. 封装全局HTTP客户端,避免重复代码
不用给每个容器单独写拦截器,所有微服务复用统一的HTTP客户端工具包,自动从当前请求上下文获取身份头:
// 全局HTTP客户端工具 export class HttpClient { static async request(config: AxiosRequestConfig, req?: Request) { const axiosInstance = axios.create() // 从当前请求上下文自动携带身份头 if (req) { axiosInstance.interceptors.request.use(requestConfig => { return { ...requestConfig, headers: { ...requestConfig.headers, Authorization: req.headers.authorization, 'X-Logged-User': req.headers['x-logged-user'] } } }) } return axiosInstance(config) } }
业务代码中调用示例:
async handle(request: RequestData) { // 跨服务调用时传递当前请求上下文 const userInfo = await HttpClient.get('/user-service/api/info', { req: request.req }) // ...业务逻辑 }
4. 可选:用请求上下文简化身份传递
如果是Node.js环境,可使用cls-hooked这类库管理请求上下文,把用户身份存入全局可访问的请求上下文,HTTP客户端自动从中获取身份,无需手动传递req对象:
// 上下文管理器 import { createNamespace } from 'cls-hooked' const requestNamespace = createNamespace('request-context') // Express中间件:将用户身份存入上下文 export const contextMiddleware = (req: Request, res: Response, next: NextFunction) => { requestNamespace.run(() => { requestNamespace.set('authorization', req.headers.authorization) requestNamespace.set('loggedUser', req.loggedUser) next() }) } // 全局HTTP客户端:自动从上下文获取身份 export class HttpClient { static async request(config: AxiosRequestConfig) { const axiosInstance = axios.create() const authorization = requestNamespace.get('authorization') if (authorization) { axiosInstance.interceptors.request.use(requestConfig => { return { ...requestConfig, headers: { ...requestConfig.headers, Authorization: authorization } } }) } return axiosInstance(config) } }
在Express中注册中间件:
app.use(contextMiddleware)
之后业务代码调用跨服务接口时,无需传递任何上下文,身份头会自动携带。
总结
- 优先用HTTP头(而非请求体)传递用户身份,网关负责透传Token或用户信息头;
- 避免全局axios拦截器,改用请求级别的实例或上下文管理,解决并发状态污染问题;
- 封装统一的HTTP客户端工具,让所有微服务复用,减少重复开发;
- 可选使用请求上下文工具,进一步简化身份传递逻辑。
内容的提问来源于stack exchange,提问作者Cleriston Martins Cardoso
相关产品推荐
相关产品推荐

