一、业务场景: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 默认不会带任何凭证。这不是"配置漏了",而是鉴权模型从根上就错了:我们试图用一个「用户身份」的凭证,去证明一次「服务身份」的行为。
它带来三个必然崩的场景:
所以正确的做法是:把"服务身份"和"用户身份"彻底拆开,各用各的凭证,各走各的通道。
下面是整个业务场景的调用链路示意:
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 的职责边界
一个跨服务的内部请求身上,其实同时携带三种信息,它们回答的是三个完全不同的问题:
| 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 个服务的日志聚合排障。
面试时的三个加分点:
七、场景题暴击
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]
网硕互联帮助中心







评论前必须登录!
注册