云计算百科
云计算领域专业知识百科平台

微服务内部接口如何鉴权?X-Internal-Key、X-User-Id 与 TraceId 的职责边界

一、业务场景:order 调 stock 为什么会返回 401?

在 order-system-cloud 里,系统被拆成 5 个进程:gateway :9000、user-service :8081、product-service :8082、order-service :8083、stock-service :8084。用户下单的链路是这样的:

Vue 前端拿着 JWT 请求 Gateway,Gateway 按路径转发给 order-service;order-service 保存订单之前,必须通过 OpenFeign 调用 stock-service 扣库存。

问题就出在这一跳:order-service 发出去的这个 HTTP 请求,不是"用户"发的,是"服务"发的。

一开始我们的做法是把用户的 JWT 一路透传下去,结果直接 401 —— stock-service 的 LoginInterceptor 把这个请求拦下来了,因为 Feign 默认不会带任何凭证。这不是"配置漏了",而是鉴权模型从根上就错了:我们试图用一个「用户身份」的凭证,去证明一次「服务身份」的行为。

它带来三个必然崩的场景:

  • 定时任务发起的调用没有用户上下文。EventRetryScheduler 每 5 秒扫一次 t_event_record 补发事件,它要调 stock-service 恢复库存 —— 这背后根本没有"当前登录用户",Token 从哪来?
  • MQ 消费者同样没有用户上下文。OrderEventConsumer 处理积分/短信/推送,也可能触发下游调用。
  • Token 过期就成了服务不可用。用户 Token 有效期是分钟级的,一旦过期,整个 order→stock 链路直接瘫痪,哪怕业务本身完全正常。
  • 所以正确的做法是:把"服务身份"和"用户身份"彻底拆开,各用各的凭证,各走各的通道。

    下面是整个业务场景的调用链路示意:

    stock-service :8084order-service :8083Gateway :9000Vue 前端stock-service :8084order-service :8083Gateway :9000Vue 前端#mermaid-svg-gen2w9B7rV0uCXq2{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gen2w9B7rV0uCXq2 .error-icon{fill:#552222;}#mermaid-svg-gen2w9B7rV0uCXq2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gen2w9B7rV0uCXq2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gen2w9B7rV0uCXq2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gen2w9B7rV0uCXq2 .marker.cross{stroke:#333333;}#mermaid-svg-gen2w9B7rV0uCXq2 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gen2w9B7rV0uCXq2 p{margin:0;}#mermaid-svg-gen2w9B7rV0uCXq2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gen2w9B7rV0uCXq2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gen2w9B7rV0uCXq2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gen2w9B7rV0uCXq2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gen2w9B7rV0uCXq2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gen2w9B7rV0uCXq2 .sequenceNumber{fill:white;}#mermaid-svg-gen2w9B7rV0uCXq2 #sequencenumber{fill:#333;}#mermaid-svg-gen2w9B7rV0uCXq2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gen2w9B7rV0uCXq2 .messageText{fill:#333;stroke:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gen2w9B7rV0uCXq2 .labelText,#mermaid-svg-gen2w9B7rV0uCXq2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .loopText,#mermaid-svg-gen2w9B7rV0uCXq2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gen2w9B7rV0uCXq2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gen2w9B7rV0uCXq2 .noteText,#mermaid-svg-gen2w9B7rV0uCXq2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gen2w9B7rV0uCXq2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gen2w9B7rV0uCXq2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gen2w9B7rV0uCXq2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gen2w9B7rV0uCXq2 .actorPopupMenu{position:absolute;}#mermaid-svg-gen2w9B7rV0uCXq2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gen2w9B7rV0uCXq2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gen2w9B7rV0uCXq2 .actor-man circle,#mermaid-svg-gen2w9B7rV0uCXq2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gen2w9B7rV0uCXq2 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}alt[凭证缺失或错误][凭证校验通过]请求下单(携带 JWT)校验 JWT,解析 userId转发请求(注入 X-User-Id / X-Trace-Id)保存订单前,准备扣库存OpenFeign 调用扣库存(携带 X-Internal-Key / X-User-Id / X-Trace-Id)401 非法调用下单失败原子 UPDATE 扣减库存扣减成功下单成功

    二、三个 Header 的职责边界

    一个跨服务的内部请求身上,其实同时携带三种信息,它们回答的是三个完全不同的问题:

    Header回答什么问题谁生成谁校验能不能作为鉴权依据
    X-Internal-Key 谁在调用?是不是一个合法的内部服务 调用方服务(FeignConfig 统一注入) 被调方服务(InternalKeyInterceptor) ✅ 唯一的鉴权依据
    X-User-Id 替谁调用?这次调用的业务归属是谁 Gateway 解析 JWT 后注入;服务间由 UserContext 透传 下游业务代码(做数据归属判断) ❌ 只是业务上下文,绝不能当凭证
    X-Trace-Id 属于哪条链路?一次用户请求串起了哪些服务 入口生成,逐跳透传 不校验,只用于日志聚合 ❌ 纯观测用途

    这张表是整个方案的核心。最容易踩的坑就是把 X-User-Id 当鉴权用 —— 它就是个普通的 HTTP 头,任何人手写一个 curl -H "X-User-Id: 1" 就能伪造。它之所以"可信",前提是这次请求已经被 X-Internal-Key 验过身份、证明它来自受信任的内部网络。

    一句话总结边界:X-Internal-Key 决定"放不放行",X-User-Id 决定"数据归谁",X-Trace-Id 决定"日志怎么串"。

    三个 Header 的职责边界可以用下面这张图来概括:

    #mermaid-svg-V3kkIkkMSpqbisoK{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V3kkIkkMSpqbisoK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V3kkIkkMSpqbisoK .error-icon{fill:#552222;}#mermaid-svg-V3kkIkkMSpqbisoK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V3kkIkkMSpqbisoK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V3kkIkkMSpqbisoK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V3kkIkkMSpqbisoK .marker.cross{stroke:#333333;}#mermaid-svg-V3kkIkkMSpqbisoK svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V3kkIkkMSpqbisoK p{margin:0;}#mermaid-svg-V3kkIkkMSpqbisoK .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-V3kkIkkMSpqbisoK .cluster-label text{fill:#333;}#mermaid-svg-V3kkIkkMSpqbisoK .cluster-label span{color:#333;}#mermaid-svg-V3kkIkkMSpqbisoK .cluster-label span p{background-color:transparent;}#mermaid-svg-V3kkIkkMSpqbisoK .label text,#mermaid-svg-V3kkIkkMSpqbisoK span{fill:#333;color:#333;}#mermaid-svg-V3kkIkkMSpqbisoK .node rect,#mermaid-svg-V3kkIkkMSpqbisoK .node circle,#mermaid-svg-V3kkIkkMSpqbisoK .node ellipse,#mermaid-svg-V3kkIkkMSpqbisoK .node polygon,#mermaid-svg-V3kkIkkMSpqbisoK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V3kkIkkMSpqbisoK .rough-node .label text,#mermaid-svg-V3kkIkkMSpqbisoK .node .label text,#mermaid-svg-V3kkIkkMSpqbisoK .image-shape .label,#mermaid-svg-V3kkIkkMSpqbisoK .icon-shape .label{text-anchor:middle;}#mermaid-svg-V3kkIkkMSpqbisoK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V3kkIkkMSpqbisoK .rough-node .label,#mermaid-svg-V3kkIkkMSpqbisoK .node .label,#mermaid-svg-V3kkIkkMSpqbisoK .image-shape .label,#mermaid-svg-V3kkIkkMSpqbisoK .icon-shape .label{text-align:center;}#mermaid-svg-V3kkIkkMSpqbisoK .node.clickable{cursor:pointer;}#mermaid-svg-V3kkIkkMSpqbisoK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V3kkIkkMSpqbisoK .arrowheadPath{fill:#333333;}#mermaid-svg-V3kkIkkMSpqbisoK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V3kkIkkMSpqbisoK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V3kkIkkMSpqbisoK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V3kkIkkMSpqbisoK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V3kkIkkMSpqbisoK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V3kkIkkMSpqbisoK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V3kkIkkMSpqbisoK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V3kkIkkMSpqbisoK .cluster text{fill:#333;}#mermaid-svg-V3kkIkkMSpqbisoK .cluster span{color:#333;}#mermaid-svg-V3kkIkkMSpqbisoK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-V3kkIkkMSpqbisoK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V3kkIkkMSpqbisoK rect.text{fill:none;stroke-width:0;}#mermaid-svg-V3kkIkkMSpqbisoK .icon-shape,#mermaid-svg-V3kkIkkMSpqbisoK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V3kkIkkMSpqbisoK .icon-shape p,#mermaid-svg-V3kkIkkMSpqbisoK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V3kkIkkMSpqbisoK .icon-shape .label rect,#mermaid-svg-V3kkIkkMSpqbisoK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V3kkIkkMSpqbisoK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V3kkIkkMSpqbisoK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V3kkIkkMSpqbisoK :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    决定放不放行

    决定数据归谁

    决定日志怎么串

    X-Internal-Key

    鉴权依据

    X-User-Id

    业务上下文

    X-Trace-Id

    观测用途

    三、全链路数据流向图

    ┌──────────────┐
    │ Vue 前端 │ Authorization: Bearer <JWT>
    └──────┬───────┘
    │ ① 用户身份:JWT
    ▼
    ┌─────────────────────────────┐
    │ Gateway :9000 │ 校验 JWT → 解析出 userId
    │ · 外部请求的唯一入口 │ → 注入 X-User-Id 向下转发
    └──────┬──────────────────────┘
    │ ② X-User-Id: 1001 X-Trace-Id: 3f2a9c
    ▼
    ┌─────────────────────────────┐
    │ order-service :8083 │ LoginInterceptor 读 X-User-Id
    │ · UserContext 保存当前用户 │ → 填充 ThreadLocal 业务上下文
    │ · 下单 / 支付 / 取消 │
    └──────┬──────────────────────┘
    │ ③ Feign 远程调用(这一段不再是"用户"发的)
    │ X-Internal-Key: <服务凭证> ← 证明"我是 order-service"
    │ X-User-Id: 1001 ← 透传业务归属,供下游判断
    │ X-Trace-Id: 3f2a9c ← 同一条链路,日志可串
    ▼
    ┌─────────────────────────────┐
    │ stock-service :8084 │ InternalKeyInterceptor 先校验凭证
    │ · /internal/** 走服务通道 │ → 通过 → 放行到业务
    │ · 原子 UPDATE 扣减库存 │ → 失败 → 401,业务代码根本不执行
    └─────────────────────────────┘

    关键设计点:stock-service 的内部接口挂在 /internal/** 路径下,和面向用户的业务接口是两个拦截器通道。 内部通道只认 X-Internal-Key,用户通道才走 JWT。两条通道物理隔离,就不会出现"服务调用被用户拦截器拦下"这种事。

    四、代码实现

    4.1 服务端:InternalKeyInterceptor 校验服务凭证

    package com.maixiaowen.common.interceptor;

    import com.maixiaowen.common.constant.AuthConstants;
    import com.maixiaowen.common.exception.BizException;
    import com.maixiaowen.common.result.ErrorCode;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.util.StringUtils;
    import org.springframework.web.servlet.HandlerInterceptor;

    import java.nio.charset.StandardCharsets;
    import java.security.MessageDigest;

    /**
    * 服务间调用凭证校验拦截器。
    * <p>
    * 只挂在 /internal/** 路径上,证明"调用方是一个合法的内部服务"。
    * 与面向用户的 LoginInterceptor 是两条互不干扰的通道。
    *
    * @author maixiaowen
    */

    @Slf4j
    public class InternalKeyInterceptor implements HandlerInterceptor {

    /** 预先把期望值转成字节数组,避免每次请求重复编码 */
    private final byte[] expectKeyBytes;

    public InternalKeyInterceptor(String internalKey) {
    // 防御式编程:宁可服务起不来,也不能裸奔上线
    if (!StringUtils.hasText(internalKey)) {
    throw new IllegalStateException("internal.auth.key 未配置,服务拒绝启动");
    }
    this.expectKeyBytes = internalKey.getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
    HttpServletResponse response,
    Object handler) {
    String actualKey = request.getHeader(AuthConstants.HEADER_INTERNAL_KEY);

    if (!StringUtils.hasText(actualKey)) {
    log.warn("[内部鉴权] 缺少 {} 头, uri={}, remoteAddr={}",
    AuthConstants.HEADER_INTERNAL_KEY,
    request.getRequestURI(), request.getRemoteAddr());
    throw new BizException(ErrorCode.UNAUTHORIZED, "非法调用:缺少服务凭证");
    }

    // 用 MessageDigest.isEqual 做定长比较:
    // 普通 String.equals 在遇到第一个不同字符时就会提前返回,
    // 攻击者可以通过响应耗时逐字节猜出凭证(时序侧信道)。
    boolean matched = MessageDigest.isEqual(
    actualKey.getBytes(StandardCharsets.UTF_8), expectKeyBytes);

    if (!matched) {
    log.warn("[内部鉴权] 服务凭证不匹配, uri={}, remoteAddr={}",
    request.getRequestURI(), request.getRemoteAddr());
    throw new BizException(ErrorCode.UNAUTHORIZED, "非法调用:服务凭证无效");
    }

    log.debug("[内部鉴权] 通过, uri={}, traceId={}",
    request.getRequestURI(), request.getHeader(AuthConstants.HEADER_TRACE_ID));
    return true;
    }
    }

    4.2 客户端:FeignConfig 统一注入三个 Header

    为什么必须放在 FeignConfig 里统一做? 因为如果让每个 Feign 接口自己写 @RequestHeader,那一定会漏 —— 项目里有 order→stock 扣库存、order→product 查商品、order→product 校验购物车三处调用,将来还会有第四处第五处。鉴权这种横切关注点,必须收敛到一个地方,漏一次就是一个 401 线上故障。

    package com.maixiaowen.order.config;

    import com.maixiaowen.common.constant.AuthConstants;
    import com.maixiaowen.common.context.UserContext;
    import com.maixiaowen.common.util.TraceIdGenerator;
    import feign.Retryer;
    import feign.RequestInterceptor;
    import org.slf4j.MDC;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.util.StringUtils;

    /**
    * OpenFeign 全局配置:把所有出站请求该带的头统一注入。
    *
    * @author maixiaowen
    */

    @Configuration
    public class FeignConfig {

    @Value("${internal.auth.key}")
    private String internalKey;

    @Bean
    public RequestInterceptor internalAuthInterceptor() {
    return template -> {
    // ① 服务身份:证明"我是 order-service,不是外部攻击者"
    // 这是唯一的鉴权凭证,被调方靠它决定放行与否
    template.header(AuthConstants.HEADER_INTERNAL_KEY, internalKey);

    // ② 用户身份:透传业务归属,供下游做"这单是不是你的"这类判断
    // 注意它只是业务上下文,不是凭证;下游绝不会拿它鉴权
    Long userId = UserContext.getUserId();
    if (userId != null) {
    template.header(AuthConstants.HEADER_USER_ID, String.valueOf(userId));
    }

    // ③ 链路追踪:同一个 TraceId 贯穿 gateway → order → stock
    String traceId = MDC.get(AuthConstants.MDC_TRACE_ID);
    template.header(AuthConstants.HEADER_TRACE_ID,
    StringUtils.hasText(traceId) ? traceId : TraceIdGenerator.next());
    };
    }

    /**
    * Feign 重试:首次间隔 100ms,最大间隔 1000ms,最多 3 次。
    * 能重试的前提是下游接口幂等 —— stock-service 用
    * t_stock_operation 的唯一键 (order_no, product_id, operation) 兜底,
    * 同一个业务操作被重放只会回放结果,绝不二次扣减。
    */

    @Bean
    public Retryer feignRetryer() {
    return new Retryer.Default(100, 1000, 3);
    }
    }

    4.3 用户通道:LoginInterceptor 与"可选依赖"

    common 模块被所有服务共用,但 stock-service 是纯库存服务,根本没有引入 Redis(它不存 Token 黑名单、不做缓存)。而 LoginInterceptor 需要 RedisTemplate 查 Token 黑名单,于是子模块启动直接报 Bean 创建失败。

    解法不是给 stock-service 硬塞一个 Redis,而是让这个依赖变成可选的:

    @Component
    public class LoginInterceptor implements HandlerInterceptor {

    /**
    * required = false:没有 Redis 的服务(如 stock-service)这里就是 null。
    * 它不处理用户通道,只跑内部服务通道,本来也用不到 Token 黑名单。
    */

    @Autowired(required = false)
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request,
    HttpServletResponse response,
    Object handler) {
    // 用户通道:读 Gateway 注入的 X-User-Id,写入 ThreadLocal 业务上下文
    // 若 redisTemplate 为空说明本服务不走用户通道,直接放行
    // …
    return true;
    }
    }

    这里的取舍值得说清楚:required = false 是把"能力缺失"显式建模,而不是掩盖问题。如果哪天 stock-service 真的需要读 Token 黑名单,注入的 redisTemplate 会是 null,运行期立刻 NPE 暴露出来 —— 这比启动期一个含糊的报错更好定位。

    4.4 生产密钥校验:默认值绝不允许上线

    服务凭证一旦用了弱默认值,等于所有内部接口对外全开。所以启动时强制校验:

    @Slf4j
    @Component
    public class InternalKeyValidator implements InitializingBean {

    /** 明令禁止用于生产的占位值 */
    private static final Set<String> FORBIDDEN =
    Set.of("dev-key", "changeme", "123456", "test");

    @Value("${internal.auth.key:}")
    private String internalKey;

    @Value("${spring.profiles.active:dev}")
    private String profile;

    @Override
    public void afterPropertiesSet() {
    if (!"prod".equals(profile)) {
    return; // 开发环境允许用简单值,方便本地调试
    }
    if (!StringUtils.hasText(internalKey)
    || FORBIDDEN.contains(internalKey.toLowerCase())
    || internalKey.length() < 32) {
    // 生产环境用人话报错,不给"改了还是起不来"的模糊提示
    throw new IllegalStateException(
    "生产环境 internal.auth.key 必须是长度 ≥32 的强随机串,当前配置不合法,服务拒绝启动");
    }
    log.info("内部服务凭证校验通过,长度={}", internalKey.length());
    }
    }

    ⚠️ 边界说明(必须诚实标注):本项目目前落地的能力是 —— 内部凭证校验 + 用户身份透传 + TraceId 跨服务透传并在日志 MDC 中打印,三者在 FeignConfig 统一注入、在 InternalKeyInterceptor 统一校验。 尚未落地的:完整的可视化链路追踪(SkyWalking / Zipkin 这类),以及凭证轮换与 mTLS。这些属于生产环境的加固项,不要当成已经做过的能力写进简历。

    下面是整个代码实现的结构总览:

    #mermaid-svg-U8LYrF9WkaTc4RWa{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U8LYrF9WkaTc4RWa .error-icon{fill:#552222;}#mermaid-svg-U8LYrF9WkaTc4RWa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U8LYrF9WkaTc4RWa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U8LYrF9WkaTc4RWa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U8LYrF9WkaTc4RWa .marker.cross{stroke:#333333;}#mermaid-svg-U8LYrF9WkaTc4RWa svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U8LYrF9WkaTc4RWa p{margin:0;}#mermaid-svg-U8LYrF9WkaTc4RWa .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa .cluster-label text{fill:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa .cluster-label span{color:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa .cluster-label span p{background-color:transparent;}#mermaid-svg-U8LYrF9WkaTc4RWa .label text,#mermaid-svg-U8LYrF9WkaTc4RWa span{fill:#333;color:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa .node rect,#mermaid-svg-U8LYrF9WkaTc4RWa .node circle,#mermaid-svg-U8LYrF9WkaTc4RWa .node ellipse,#mermaid-svg-U8LYrF9WkaTc4RWa .node polygon,#mermaid-svg-U8LYrF9WkaTc4RWa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-U8LYrF9WkaTc4RWa .rough-node .label text,#mermaid-svg-U8LYrF9WkaTc4RWa .node .label text,#mermaid-svg-U8LYrF9WkaTc4RWa .image-shape .label,#mermaid-svg-U8LYrF9WkaTc4RWa .icon-shape .label{text-anchor:middle;}#mermaid-svg-U8LYrF9WkaTc4RWa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U8LYrF9WkaTc4RWa .rough-node .label,#mermaid-svg-U8LYrF9WkaTc4RWa .node .label,#mermaid-svg-U8LYrF9WkaTc4RWa .image-shape .label,#mermaid-svg-U8LYrF9WkaTc4RWa .icon-shape .label{text-align:center;}#mermaid-svg-U8LYrF9WkaTc4RWa .node.clickable{cursor:pointer;}#mermaid-svg-U8LYrF9WkaTc4RWa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-U8LYrF9WkaTc4RWa .arrowheadPath{fill:#333333;}#mermaid-svg-U8LYrF9WkaTc4RWa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-U8LYrF9WkaTc4RWa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-U8LYrF9WkaTc4RWa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U8LYrF9WkaTc4RWa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-U8LYrF9WkaTc4RWa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U8LYrF9WkaTc4RWa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-U8LYrF9WkaTc4RWa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-U8LYrF9WkaTc4RWa .cluster text{fill:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa .cluster span{color:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U8LYrF9WkaTc4RWa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U8LYrF9WkaTc4RWa rect.text{fill:none;stroke-width:0;}#mermaid-svg-U8LYrF9WkaTc4RWa .icon-shape,#mermaid-svg-U8LYrF9WkaTc4RWa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U8LYrF9WkaTc4RWa .icon-shape p,#mermaid-svg-U8LYrF9WkaTc4RWa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-U8LYrF9WkaTc4RWa .icon-shape .label rect,#mermaid-svg-U8LYrF9WkaTc4RWa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U8LYrF9WkaTc4RWa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U8LYrF9WkaTc4RWa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U8LYrF9WkaTc4RWa :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    服务端 stock-service

    客户端 order-service

    X-Internal-Key / X-User-Id / X-Trace-Id

    通过

    失败

    启动时校验

    FeignConfig统一注入三个 Header

    InternalKeyInterceptor校验 X-Internal-Key

    LoginInterceptor读取 X-User-Id

    InternalKeyValidator生产密钥强校验

    401 拒绝

    五、异常与并发边界

    边界场景会发生什么本项目的处理
    Feign 请求没带凭证 被调方 401,业务代码根本不执行 FeignConfig 统一注入,杜绝"某个接口忘了写"
    内部凭证用弱默认值 等于内部接口全开 InternalKeyValidator 生产环境拒绝启动
    凭证比对被时序攻击逐字节猜解 凭证泄露 MessageDigest.isEqual 定长比较
    定时任务 / MQ 消费者发起调用 UserContext 为空,没有登录用户 X-User-Id 为空时不注入该头,下游走"系统调用"语义;X-Trace-Id 兜底新生成,保证链路不断
    下游服务没引入 Redis 公共模块 Bean 创建失败,服务起不来 @Autowired(required = false) 让依赖可选
    Feign 超时重试(最多 3 次) 同一个业务操作被重复执行 下游用幂等键 t_stock_operation(order_no, product_id, operation) 兜底,重试 = 无害回放
    凭证泄露后要换 key 全量服务重启,期间不可用 尚未落地:需要配合配置中心动态刷新 + 双 key 灰度过渡

    注意第 4 条和第 6 条:它们是同一个设计原则的两面 —— 内部通道的可信度由 X-Internal-Key 保证,而重复调用带来的业务风险由幂等键保证。鉴权解决"能不能调",幂等解决"调多次会怎样",两件事不能混。

    异常与并发边界的处理逻辑可以归纳为下面这张决策图:

    #mermaid-svg-RcljuNDTFjdnT92X{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RcljuNDTFjdnT92X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RcljuNDTFjdnT92X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RcljuNDTFjdnT92X .error-icon{fill:#552222;}#mermaid-svg-RcljuNDTFjdnT92X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RcljuNDTFjdnT92X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RcljuNDTFjdnT92X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RcljuNDTFjdnT92X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RcljuNDTFjdnT92X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RcljuNDTFjdnT92X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RcljuNDTFjdnT92X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RcljuNDTFjdnT92X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RcljuNDTFjdnT92X .marker.cross{stroke:#333333;}#mermaid-svg-RcljuNDTFjdnT92X svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RcljuNDTFjdnT92X p{margin:0;}#mermaid-svg-RcljuNDTFjdnT92X .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-RcljuNDTFjdnT92X .cluster-label text{fill:#333;}#mermaid-svg-RcljuNDTFjdnT92X .cluster-label span{color:#333;}#mermaid-svg-RcljuNDTFjdnT92X .cluster-label span p{background-color:transparent;}#mermaid-svg-RcljuNDTFjdnT92X .label text,#mermaid-svg-RcljuNDTFjdnT92X span{fill:#333;color:#333;}#mermaid-svg-RcljuNDTFjdnT92X .node rect,#mermaid-svg-RcljuNDTFjdnT92X .node circle,#mermaid-svg-RcljuNDTFjdnT92X .node ellipse,#mermaid-svg-RcljuNDTFjdnT92X .node polygon,#mermaid-svg-RcljuNDTFjdnT92X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RcljuNDTFjdnT92X .rough-node .label text,#mermaid-svg-RcljuNDTFjdnT92X .node .label text,#mermaid-svg-RcljuNDTFjdnT92X .image-shape .label,#mermaid-svg-RcljuNDTFjdnT92X .icon-shape .label{text-anchor:middle;}#mermaid-svg-RcljuNDTFjdnT92X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RcljuNDTFjdnT92X .rough-node .label,#mermaid-svg-RcljuNDTFjdnT92X .node .label,#mermaid-svg-RcljuNDTFjdnT92X .image-shape .label,#mermaid-svg-RcljuNDTFjdnT92X .icon-shape .label{text-align:center;}#mermaid-svg-RcljuNDTFjdnT92X .node.clickable{cursor:pointer;}#mermaid-svg-RcljuNDTFjdnT92X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RcljuNDTFjdnT92X .arrowheadPath{fill:#333333;}#mermaid-svg-RcljuNDTFjdnT92X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RcljuNDTFjdnT92X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RcljuNDTFjdnT92X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RcljuNDTFjdnT92X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RcljuNDTFjdnT92X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RcljuNDTFjdnT92X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RcljuNDTFjdnT92X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RcljuNDTFjdnT92X .cluster text{fill:#333;}#mermaid-svg-RcljuNDTFjdnT92X .cluster span{color:#333;}#mermaid-svg-RcljuNDTFjdnT92X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RcljuNDTFjdnT92X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RcljuNDTFjdnT92X rect.text{fill:none;stroke-width:0;}#mermaid-svg-RcljuNDTFjdnT92X .icon-shape,#mermaid-svg-RcljuNDTFjdnT92X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RcljuNDTFjdnT92X .icon-shape p,#mermaid-svg-RcljuNDTFjdnT92X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RcljuNDTFjdnT92X .icon-shape .label rect,#mermaid-svg-RcljuNDTFjdnT92X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RcljuNDTFjdnT92X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RcljuNDTFjdnT92X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RcljuNDTFjdnT92X :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    否

    是

    否

    是

    否

    是

    Feign 出站请求

    是否携带 X-Internal-Key?

    被调方 401业务代码不执行

    凭证是否匹配?

    是否有用户上下文?

    不注入 X-User-Id走系统调用语义

    注入 X-User-Id透传业务归属

    下游幂等键兜底重试 = 无害回放

    六、面试 / 简历亮点提炼

    简历怎么写(可直接用):

    设计并实现微服务内部接口鉴权体系:基于 OpenFeign RequestInterceptor 统一注入 X-Internal-Key 服务凭证,与用户 JWT 通道物理分离,解决定时任务 / MQ 消费者等无用户上下文场景的调用鉴权问题;凭证比对采用定长比较防时序攻击,生产环境强制强随机密钥校验。同步落地 X-User-Id 业务上下文透传与 X-Trace-Id 全链路透传,支撑跨 5 个服务的日志聚合排障。

    面试时的三个加分点:

  • 你踩过坑并且知道根因:能讲清"为什么透传用户 Token 做服务间鉴权是模型错误",而不是"Feign 忘了加 header"。
  • 你有安全敏感度:主动提到 MessageDigest.isEqual 防时序攻击、生产禁弱密钥。这两个细节大部分应届生答不出来。
  • 你知道边界在哪:能主动说出"凭证本身没有轮换机制、没有 mTLS,这是我知道但没做的",比全说"都做了"更可信。
  • 七、场景题暴击

    Q1:X-Internal-Key 泄露了怎么办?如何做到"可轮换、不重启"?

    泄露的后果比用户 Token 泄露更严重 —— 攻击者可以调用所有内部接口,不只是某个用户的权限范围。所以防护要分层:网络层(内部接口不暴露公网,只在内网 / Service Mesh 内可达)、凭证层(定期轮换)、最小权限层(内部凭证只能调 /internal/**,调不了管理接口)。

    轮换不重启的经典做法是双 key 灰度:配置里放 internal.auth.keys(一个列表),服务端校验时只要命中列表中任意一个就放行;轮换流程是「先给所有服务下发新 key 列表(含新旧两个)→ 等全量生效 → 调用方切到新 key → 再摘掉旧 key」。整个过程任何一刻都有至少一个 key 是双方共识的,所以零停机。

    Q2:定时任务发起的 Feign 调用没有登录用户,X-User-Id 该传什么?下游如果按 userId 做数据归属校验会怎样?

    这正是本项目里 X-User-Id 为空时不注入该头的场景。关键在于:下游绝不能把"没有 X-User-Id"理解成"userId = null 的用户",而必须显式区分两种语义 —— 「用户发起的调用」和「系统发起的调用」。

    如果下游校验写成 if (!order.getUserId().equals(currentUserId)) throw 403;,那么系统调用会直接 403,或者更糟 —— 触发 NPE。正确做法有两种:一是下游对系统调用走独立的内部接口,天然不做用户维度校验;二是引入显式的 SYSTEM 主体(比如 X-Caller-Type: SERVICE),让"系统身份"成为一个一等公民,而不是缺失值。

    这题真正考的是:你有没有把「缺失」和「特权」区分开。 很多线上越权漏洞的根因就是 —— 代码把"没有用户上下文"当成了"可以跳过校验",而不是"必须拒绝"。

    推荐标签:微服务、SpringCloud、OpenFeign、鉴权、链路追踪

    场景题里提到的「双 key 灰度轮换」流程,用图表示更直观:

    被调方服务调用方服务配置中心被调方服务调用方服务配置中心#mermaid-svg-oA3JSJ0T7xX5TRBD{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oA3JSJ0T7xX5TRBD .error-icon{fill:#552222;}#mermaid-svg-oA3JSJ0T7xX5TRBD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oA3JSJ0T7xX5TRBD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oA3JSJ0T7xX5TRBD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oA3JSJ0T7xX5TRBD .marker.cross{stroke:#333333;}#mermaid-svg-oA3JSJ0T7xX5TRBD svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oA3JSJ0T7xX5TRBD p{margin:0;}#mermaid-svg-oA3JSJ0T7xX5TRBD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oA3JSJ0T7xX5TRBD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-oA3JSJ0T7xX5TRBD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-oA3JSJ0T7xX5TRBD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-oA3JSJ0T7xX5TRBD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-oA3JSJ0T7xX5TRBD .sequenceNumber{fill:white;}#mermaid-svg-oA3JSJ0T7xX5TRBD #sequencenumber{fill:#333;}#mermaid-svg-oA3JSJ0T7xX5TRBD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-oA3JSJ0T7xX5TRBD .messageText{fill:#333;stroke:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oA3JSJ0T7xX5TRBD .labelText,#mermaid-svg-oA3JSJ0T7xX5TRBD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .loopText,#mermaid-svg-oA3JSJ0T7xX5TRBD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-oA3JSJ0T7xX5TRBD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-oA3JSJ0T7xX5TRBD .noteText,#mermaid-svg-oA3JSJ0T7xX5TRBD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-oA3JSJ0T7xX5TRBD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oA3JSJ0T7xX5TRBD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oA3JSJ0T7xX5TRBD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oA3JSJ0T7xX5TRBD .actorPopupMenu{position:absolute;}#mermaid-svg-oA3JSJ0T7xX5TRBD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-oA3JSJ0T7xX5TRBD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oA3JSJ0T7xX5TRBD .actor-man circle,#mermaid-svg-oA3JSJ0T7xX5TRBD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-oA3JSJ0T7xX5TRBD :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}阶段一:下发新 key(新旧并存)阶段二:调用方切到新 key阶段三:摘掉旧 key全程零停机,任何一刻都有共识 key下发 key 列表 [old, new]下发 key 列表 [old, new]携带 X-Internal-Key: new命中列表任意一个即放行下发 key 列表 [new]下发 key 列表 [new]

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 微服务内部接口如何鉴权?X-Internal-Key、X-User-Id 与 TraceId 的职责边界
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!