You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express Gateway授权:服务间调用loggedUser传递方案咨询

微服务间用户身份传递的最佳方案

问题背景

我的架构是网关统一处理所有请求,验证JWT Token后将loggedUser添加到请求体中,再转发到对应的业务容器。现在遇到两个核心问题:

  1. 容器之间互相调用时,loggedUser仅存在于第一个接收请求的容器,无法传递到后续调用的其他容器;
  2. 尝试给每个容器添加拦截器自动携带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

问题根源分析

  1. 拦截器设计缺陷:当前InterceptorAdapter使用静态属性存储请求拦截器,且每次调用interceptRequest()都会清空全局axios拦截器,多请求并发时,不同请求的Authorization头会互相覆盖,导致旧值残留;
  2. 身份传递方式错误:网关把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)

之后业务代码调用跨服务接口时,无需传递任何上下文,身份头会自动携带。


总结

  1. 优先用HTTP头(而非请求体)传递用户身份,网关负责透传Token或用户信息头;
  2. 避免全局axios拦截器,改用请求级别的实例或上下文管理,解决并发状态污染问题;
  3. 封装统一的HTTP客户端工具,让所有微服务复用,减少重复开发;
  4. 可选使用请求上下文工具,进一步简化身份传递逻辑。

内容的提问来源于stack exchange,提问作者Cleriston Martins Cardoso

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 13:17:23