OpenAPI 网关:鉴权 + 流量治理 一体化设计方案与实现

非独立网关,属于历史版本限制下的优化方案

适用范围:任何基于 Servlet 容器(war 部署)的单体 Spring Boot 应用,需要对开放 API 进行「身份鉴权 + 授权控制 + 流量治理」

依赖:Spring Boot 2.x(Servlet 栈)、Guava(可选,用于限流)、MySQL(可选,用于授权关系存储)


一、设计目标与定位

在单体应用内部,为对外开放的 API(路径统一前缀,如 /api/v1/*)提供统一的:

  1. 身份认证(Authentication)------识别调用方是谁(RSA 签名验签)。
  2. 授权控制(Authorization)------判断调用方是否有权访问该 API。
  3. 流量治理(Traffic Governance)------限流、熔断、降级,保护后端不被拖垮。

核心原则 :不引入 Spring Cloud Gateway / Sentinel 等重框架,全部基于 Servlet Filter 链 + 数据库表驱动配置 实现,与现有 war 部署体系零冲突,可平滑移植。


二、整体架构

2.1 Filter 链顺序(关键)

#mermaid-svg-qOFBBx8kvAYK1GCn{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-qOFBBx8kvAYK1GCn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qOFBBx8kvAYK1GCn .error-icon{fill:#552222;}#mermaid-svg-qOFBBx8kvAYK1GCn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qOFBBx8kvAYK1GCn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qOFBBx8kvAYK1GCn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qOFBBx8kvAYK1GCn .marker.cross{stroke:#333333;}#mermaid-svg-qOFBBx8kvAYK1GCn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qOFBBx8kvAYK1GCn p{margin:0;}#mermaid-svg-qOFBBx8kvAYK1GCn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn .cluster-label text{fill:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn .cluster-label span{color:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn .cluster-label span p{background-color:transparent;}#mermaid-svg-qOFBBx8kvAYK1GCn .label text,#mermaid-svg-qOFBBx8kvAYK1GCn span{fill:#333;color:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn .node rect,#mermaid-svg-qOFBBx8kvAYK1GCn .node circle,#mermaid-svg-qOFBBx8kvAYK1GCn .node ellipse,#mermaid-svg-qOFBBx8kvAYK1GCn .node polygon,#mermaid-svg-qOFBBx8kvAYK1GCn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qOFBBx8kvAYK1GCn .rough-node .label text,#mermaid-svg-qOFBBx8kvAYK1GCn .node .label text,#mermaid-svg-qOFBBx8kvAYK1GCn .image-shape .label,#mermaid-svg-qOFBBx8kvAYK1GCn .icon-shape .label{text-anchor:middle;}#mermaid-svg-qOFBBx8kvAYK1GCn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qOFBBx8kvAYK1GCn .rough-node .label,#mermaid-svg-qOFBBx8kvAYK1GCn .node .label,#mermaid-svg-qOFBBx8kvAYK1GCn .image-shape .label,#mermaid-svg-qOFBBx8kvAYK1GCn .icon-shape .label{text-align:center;}#mermaid-svg-qOFBBx8kvAYK1GCn .node.clickable{cursor:pointer;}#mermaid-svg-qOFBBx8kvAYK1GCn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qOFBBx8kvAYK1GCn .arrowheadPath{fill:#333333;}#mermaid-svg-qOFBBx8kvAYK1GCn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qOFBBx8kvAYK1GCn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qOFBBx8kvAYK1GCn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qOFBBx8kvAYK1GCn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qOFBBx8kvAYK1GCn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qOFBBx8kvAYK1GCn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qOFBBx8kvAYK1GCn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qOFBBx8kvAYK1GCn .cluster text{fill:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn .cluster span{color:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn 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-qOFBBx8kvAYK1GCn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qOFBBx8kvAYK1GCn rect.text{fill:none;stroke-width:0;}#mermaid-svg-qOFBBx8kvAYK1GCn .icon-shape,#mermaid-svg-qOFBBx8kvAYK1GCn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qOFBBx8kvAYK1GCn .icon-shape p,#mermaid-svg-qOFBBx8kvAYK1GCn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qOFBBx8kvAYK1GCn .icon-shape .label rect,#mermaid-svg-qOFBBx8kvAYK1GCn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qOFBBx8kvAYK1GCn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qOFBBx8kvAYK1GCn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qOFBBx8kvAYK1GCn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求 /api/v1/*
RateLimitFilter

限流 · 令牌桶按 systemName 维度
CircuitBreakerFilter

熔断 · 失败率阈值
OpenApiFilter

身份认证 + 授权 · RSA 验签
FallbackFilter

降级 · 异常兜底
Controller 业务

顺序设计理由:

  • 限流在最前:令牌桶获取失败是 O(1) 操作,把无效流量挡在最外层。
  • 熔断在鉴权前:熔断器打开时直接快速失败,避免无效的验签与 DB 查询。
  • 降级在最后:包裹业务执行,统一兜底业务异常。

2.2 模块边界

层 职责 是否可独立复用
Filter 层 拦截、判定、放行/拒绝 ✅ 完全复用
配置层 治理配置读取(DB / 内存) ✅ 抽象接口,可替换
存储层 授权关系、治理配置持久化 ⚠️ 表结构随项目定制

三、核心概念与数据模型

3.1 核心概念

概念 说明
OpenAPI 一条对外开放的 API,以 URL 唯一标识,含灰度、治理配置
第三方系统(System) API 的调用方,持有密钥对(公钥/私钥)
授权关系(Rel) 系统 ↔ API 的绑定关系,含是否验签开关
灰度(Grey) 该API 跳过所有校验(含参数存在性),用于过渡
治理配置(Governance) 每个 API 的限流/熔断/降级参数

3.2 数据模型(ER)

核心字段
#mermaid-svg-xWiwaGm7IhNA7Gqx{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-xWiwaGm7IhNA7Gqx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xWiwaGm7IhNA7Gqx .error-icon{fill:#552222;}#mermaid-svg-xWiwaGm7IhNA7Gqx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xWiwaGm7IhNA7Gqx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xWiwaGm7IhNA7Gqx .marker.cross{stroke:#333333;}#mermaid-svg-xWiwaGm7IhNA7Gqx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xWiwaGm7IhNA7Gqx p{margin:0;}#mermaid-svg-xWiwaGm7IhNA7Gqx .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-xWiwaGm7IhNA7Gqx .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-xWiwaGm7IhNA7Gqx .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-xWiwaGm7IhNA7Gqx .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-xWiwaGm7IhNA7Gqx .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-xWiwaGm7IhNA7Gqx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-xWiwaGm7IhNA7Gqx .node rect,#mermaid-svg-xWiwaGm7IhNA7Gqx .node circle,#mermaid-svg-xWiwaGm7IhNA7Gqx .node ellipse,#mermaid-svg-xWiwaGm7IhNA7Gqx .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xWiwaGm7IhNA7Gqx .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-xWiwaGm7IhNA7Gqx .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xWiwaGm7IhNA7Gqx .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xWiwaGm7IhNA7Gqx .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-xWiwaGm7IhNA7Gqx .edgeLabel .label text{fill:#333;}#mermaid-svg-xWiwaGm7IhNA7Gqx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 申请授权
被授权
治理配置
t_third_system_info
bigint
id
PK
varchar
third_system_name
系统简称(唯一)
text
public_key
公钥
text
private_key
私钥
t_sys_api_rel
bigint
id
PK
bigint
third_system_id
FK
bigint
open_api_id
FK
tinyint
is_sign
是否验签
t_open_api
bigint
id
PK
varchar
api_url
API地址(唯一)
varchar
description
简要描述
tinyint
is_grey
灰度标记
t_api_governance
bigint
id
PK
bigint
open_api_id
FK
int
rate_limit_qps
限流QPS,0=不限
tinyint
circuit_threshold
熔断失败率阈值%
tinyint
degrade_enabled
降级开关


四、鉴权设计

4.1 授权流程

#mermaid-svg-mskGDQPAaxuBMj8L{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-mskGDQPAaxuBMj8L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mskGDQPAaxuBMj8L .error-icon{fill:#552222;}#mermaid-svg-mskGDQPAaxuBMj8L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mskGDQPAaxuBMj8L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mskGDQPAaxuBMj8L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mskGDQPAaxuBMj8L .marker.cross{stroke:#333333;}#mermaid-svg-mskGDQPAaxuBMj8L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mskGDQPAaxuBMj8L p{margin:0;}#mermaid-svg-mskGDQPAaxuBMj8L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mskGDQPAaxuBMj8L .cluster-label text{fill:#333;}#mermaid-svg-mskGDQPAaxuBMj8L .cluster-label span{color:#333;}#mermaid-svg-mskGDQPAaxuBMj8L .cluster-label span p{background-color:transparent;}#mermaid-svg-mskGDQPAaxuBMj8L .label text,#mermaid-svg-mskGDQPAaxuBMj8L span{fill:#333;color:#333;}#mermaid-svg-mskGDQPAaxuBMj8L .node rect,#mermaid-svg-mskGDQPAaxuBMj8L .node circle,#mermaid-svg-mskGDQPAaxuBMj8L .node ellipse,#mermaid-svg-mskGDQPAaxuBMj8L .node polygon,#mermaid-svg-mskGDQPAaxuBMj8L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mskGDQPAaxuBMj8L .rough-node .label text,#mermaid-svg-mskGDQPAaxuBMj8L .node .label text,#mermaid-svg-mskGDQPAaxuBMj8L .image-shape .label,#mermaid-svg-mskGDQPAaxuBMj8L .icon-shape .label{text-anchor:middle;}#mermaid-svg-mskGDQPAaxuBMj8L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mskGDQPAaxuBMj8L .rough-node .label,#mermaid-svg-mskGDQPAaxuBMj8L .node .label,#mermaid-svg-mskGDQPAaxuBMj8L .image-shape .label,#mermaid-svg-mskGDQPAaxuBMj8L .icon-shape .label{text-align:center;}#mermaid-svg-mskGDQPAaxuBMj8L .node.clickable{cursor:pointer;}#mermaid-svg-mskGDQPAaxuBMj8L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mskGDQPAaxuBMj8L .arrowheadPath{fill:#333333;}#mermaid-svg-mskGDQPAaxuBMj8L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mskGDQPAaxuBMj8L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mskGDQPAaxuBMj8L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mskGDQPAaxuBMj8L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mskGDQPAaxuBMj8L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mskGDQPAaxuBMj8L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mskGDQPAaxuBMj8L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mskGDQPAaxuBMj8L .cluster text{fill:#333;}#mermaid-svg-mskGDQPAaxuBMj8L .cluster span{color:#333;}#mermaid-svg-mskGDQPAaxuBMj8L 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-mskGDQPAaxuBMj8L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mskGDQPAaxuBMj8L rect.text{fill:none;stroke-width:0;}#mermaid-svg-mskGDQPAaxuBMj8L .icon-shape,#mermaid-svg-mskGDQPAaxuBMj8L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mskGDQPAaxuBMj8L .icon-shape p,#mermaid-svg-mskGDQPAaxuBMj8L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mskGDQPAaxuBMj8L .icon-shape .label rect,#mermaid-svg-mskGDQPAaxuBMj8L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mskGDQPAaxuBMj8L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mskGDQPAaxuBMj8L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mskGDQPAaxuBMj8L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 登记第三方系统
生成 RSA 密钥对
私钥提供给第三方
为第三方登记 OpenAPI 授权
第三方调用时携带 sign + systemName

4.2 鉴权流程

#mermaid-svg-OazxYz7yxsUIrs4F{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-OazxYz7yxsUIrs4F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OazxYz7yxsUIrs4F .error-icon{fill:#552222;}#mermaid-svg-OazxYz7yxsUIrs4F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OazxYz7yxsUIrs4F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OazxYz7yxsUIrs4F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OazxYz7yxsUIrs4F .marker.cross{stroke:#333333;}#mermaid-svg-OazxYz7yxsUIrs4F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OazxYz7yxsUIrs4F p{margin:0;}#mermaid-svg-OazxYz7yxsUIrs4F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OazxYz7yxsUIrs4F .cluster-label text{fill:#333;}#mermaid-svg-OazxYz7yxsUIrs4F .cluster-label span{color:#333;}#mermaid-svg-OazxYz7yxsUIrs4F .cluster-label span p{background-color:transparent;}#mermaid-svg-OazxYz7yxsUIrs4F .label text,#mermaid-svg-OazxYz7yxsUIrs4F span{fill:#333;color:#333;}#mermaid-svg-OazxYz7yxsUIrs4F .node rect,#mermaid-svg-OazxYz7yxsUIrs4F .node circle,#mermaid-svg-OazxYz7yxsUIrs4F .node ellipse,#mermaid-svg-OazxYz7yxsUIrs4F .node polygon,#mermaid-svg-OazxYz7yxsUIrs4F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OazxYz7yxsUIrs4F .rough-node .label text,#mermaid-svg-OazxYz7yxsUIrs4F .node .label text,#mermaid-svg-OazxYz7yxsUIrs4F .image-shape .label,#mermaid-svg-OazxYz7yxsUIrs4F .icon-shape .label{text-anchor:middle;}#mermaid-svg-OazxYz7yxsUIrs4F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OazxYz7yxsUIrs4F .rough-node .label,#mermaid-svg-OazxYz7yxsUIrs4F .node .label,#mermaid-svg-OazxYz7yxsUIrs4F .image-shape .label,#mermaid-svg-OazxYz7yxsUIrs4F .icon-shape .label{text-align:center;}#mermaid-svg-OazxYz7yxsUIrs4F .node.clickable{cursor:pointer;}#mermaid-svg-OazxYz7yxsUIrs4F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OazxYz7yxsUIrs4F .arrowheadPath{fill:#333333;}#mermaid-svg-OazxYz7yxsUIrs4F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OazxYz7yxsUIrs4F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OazxYz7yxsUIrs4F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OazxYz7yxsUIrs4F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OazxYz7yxsUIrs4F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OazxYz7yxsUIrs4F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OazxYz7yxsUIrs4F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OazxYz7yxsUIrs4F .cluster text{fill:#333;}#mermaid-svg-OazxYz7yxsUIrs4F .cluster span{color:#333;}#mermaid-svg-OazxYz7yxsUIrs4F 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-OazxYz7yxsUIrs4F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OazxYz7yxsUIrs4F rect.text{fill:none;stroke-width:0;}#mermaid-svg-OazxYz7yxsUIrs4F .icon-shape,#mermaid-svg-OazxYz7yxsUIrs4F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OazxYz7yxsUIrs4F .icon-shape p,#mermaid-svg-OazxYz7yxsUIrs4F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OazxYz7yxsUIrs4F .icon-shape .label rect,#mermaid-svg-OazxYz7yxsUIrs4F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OazxYz7yxsUIrs4F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OazxYz7yxsUIrs4F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OazxYz7yxsUIrs4F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
否
是
不一致
一致
第三方调用 /api/v1/*
灰度判定 is_grey=1?
直接放行
提取 sign、systemName
系统是否登记

且 API 是否授权?
返回 系统或API未登记
是否开启验签?
公钥解密 sign 比对 systemName
返回 API鉴权未通过

4.3 关键点

  • 签名算法 :RSA(SHA512withRSA),sign 为 systemName 原文的签名。
  • 参数来源:支持 query 参数和 JSON body 两种(body 需缓存可重复读)。
  • 灰度兜底 :is_grey=1 时跳过所有校验,兼容历史已开放未传参的 API。

五、流量治理设计

5.1 限流(Rate Limit)

项 设计
算法 令牌桶(复用 Guava RateLimiter)
维度 按 systemName(每个调用方独立限流)
触发 令牌获取失败(QPS 超阈值)
行为 返回 HTTP 429 + 统一错误体 {code:1008, msg:"请求过于频繁"}

5.2 熔断(Circuit Breaker)

项 设计
算法 滑动计数窗口(失败率阈值)
状态机 关闭(Closed) → 打开(Open) → 半开(Half-Open)
触发 窗口内失败率 > circuit_threshold
行为 打开后快速失败,不调用后端;半开探测恢复

#mermaid-svg-Ka73n5rIvHc4xUcn{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-Ka73n5rIvHc4xUcn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ka73n5rIvHc4xUcn .error-icon{fill:#552222;}#mermaid-svg-Ka73n5rIvHc4xUcn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ka73n5rIvHc4xUcn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ka73n5rIvHc4xUcn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ka73n5rIvHc4xUcn .marker.cross{stroke:#333333;}#mermaid-svg-Ka73n5rIvHc4xUcn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ka73n5rIvHc4xUcn p{margin:0;}#mermaid-svg-Ka73n5rIvHc4xUcn defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-Ka73n5rIvHc4xUcn g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-Ka73n5rIvHc4xUcn g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-Ka73n5rIvHc4xUcn g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-Ka73n5rIvHc4xUcn g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Ka73n5rIvHc4xUcn g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-Ka73n5rIvHc4xUcn .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Ka73n5rIvHc4xUcn .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-Ka73n5rIvHc4xUcn .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-Ka73n5rIvHc4xUcn .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Ka73n5rIvHc4xUcn .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-Ka73n5rIvHc4xUcn .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-Ka73n5rIvHc4xUcn .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-Ka73n5rIvHc4xUcn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ka73n5rIvHc4xUcn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ka73n5rIvHc4xUcn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ka73n5rIvHc4xUcn .edgeLabel .label text{fill:#333;}#mermaid-svg-Ka73n5rIvHc4xUcn .label div .edgeLabel{color:#333;}#mermaid-svg-Ka73n5rIvHc4xUcn .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-Ka73n5rIvHc4xUcn .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-Ka73n5rIvHc4xUcn .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-Ka73n5rIvHc4xUcn .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-Ka73n5rIvHc4xUcn .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-Ka73n5rIvHc4xUcn .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ka73n5rIvHc4xUcn .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ka73n5rIvHc4xUcn #statediagram-barbEnd{fill:#333333;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ka73n5rIvHc4xUcn .cluster-label,#mermaid-svg-Ka73n5rIvHc4xUcn .nodeLabel{color:#131300;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-Ka73n5rIvHc4xUcn .note-edge{stroke-dasharray:5;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-note text{fill:black;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram-note .nodeLabel{color:black;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagram .edgeLabel{color:red;}#mermaid-svg-Ka73n5rIvHc4xUcn #dependencyStart,#mermaid-svg-Ka73n5rIvHc4xUcn #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-Ka73n5rIvHc4xUcn .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ka73n5rIvHc4xUcn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败率超阈值
超冷却时间
探测成功
探测失败
关闭Closed
打开Open
半开HalfOpen

5.3 降级(Fallback)

项 设计
触发 业务异常 / 超时 / 熔断打开
行为 返回预定义兜底 JSON,而非抛异常堆栈

5.4 分布式一致性(重要)

限流/熔断的计数器若存 JVM 内存,多实例部署时会失效(每个实例独立计数)。

方案:计数器抽象为接口,提供两种实现:

  • 单机实现 (默认):ConcurrentHashMap + Guava RateLimiter。
  • Redis 实现(多实例时切换):基于 Lua 脚本的令牌桶 / 计数器。

通过配置项一键切换,业务代码无感知。


六、代码实现

6.1 治理配置模型

java 复制代码
package com.example.gateway.model;

import lombok.Data;

/**
 * 单个 API 的流量治理配置
 */
@Data
public class GovernanceConfig {
    /** API URL */
    private String apiUrl;
    /** 限流 QPS,0 表示不限流 */
    private int rateLimitQps;
    /** 熔断失败率阈值(百分比,如 50 表示 50%) */
    private int circuitThreshold;
    /** 是否启用降级 */
    private boolean degradeEnabled;
    /** 熔断半开状态探测间隔(毫秒) */
    private long circuitHalfOpenMs = 30000;
    /** 熔断统计窗口(毫秒) */
    private long circuitWindowMs = 60000;
}

6.2 治理配置服务(抽象接口,便于替换存储)

java 复制代码
package com.example.gateway.service;

/**
 * 治理配置读取服务。
 * 默认实现从内存 Map 读取(启动时加载),可替换为 DB / SCM 动态读取。
 */
public interface GovernanceConfigService {
    GovernanceConfig getByApiUrl(String apiUrl);
}
java 复制代码
package com.example.gateway.service;

import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 基于内存的治理配置实现(示例)。
 * 生产环境建议从 DB / SCM 配置中心加载,并支持定时刷新。
 */
@Service
public class InMemoryGovernanceConfigService implements GovernanceConfigService {

    private final Map<String, GovernanceConfig> cache = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        // 示例:可从 t_api_governance 表加载
        // List<GovernanceConfig> list = governanceMapper.listAll();
        // list.forEach(c -> cache.put(c.getApiUrl(), c));
    }

    @Override
    public GovernanceConfig getByApiUrl(String apiUrl) {
        return cache.get(apiUrl);
    }
}

6.3 统一错误响应工具

java 复制代码
package com.example.gateway.support;

import com.alibaba.fastjson2.JSON;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 统一网关错误响应。
 * 所有 Filter 拒绝请求时统一输出 {code, msg, data} 结构。
 */
public final class GatewayResponse {

    private GatewayResponse() {
    }

    public static void failure(HttpServletResponse res, int code, String msg) throws IOException {
        Map<String, Object> map = new HashMap<>(3);
        map.put("code", code);
        map.put("msg", msg);
        map.put("data", "");
        res.setCharacterEncoding("UTF-8");
        res.setHeader("Content-Type", "application/json;charset=UTF-8");
        PrintWriter pw = res.getWriter();
        pw.print(JSON.toJSONString(map));
        pw.close();
    }
}

6.4 限流 Filter

java 复制代码
package com.example.gateway.filter;

import com.example.gateway.model.GovernanceConfig;
import com.example.gateway.service.GovernanceConfigService;
import com.example.gateway.support.GatewayResponse;
import com.google.common.util.concurrent.RateLimiter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 限流 Filter:按 systemName 维度限流(令牌桶)。
 * 通过 Guava RateLimiter 实现,无外部依赖。
 */
@WebFilter(urlPatterns = "/api/v1/*", filterName = "RateLimitFilter")
public class RateLimitFilter implements Filter {

    /** systemName -> RateLimiter,每个调用方独立令牌桶 */
    private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();

    private GovernanceConfigService configService;

    @Override
    public void init(FilterConfig filterConfig) {
        // 从 Spring 上下文获取 Bean(Filter 无法直接 @Autowired)
        configService = SpringContextHolder.getBean(GovernanceConfigService.class);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String systemName = extractSystemName(request);
        if (systemName == null) {
            // 未携带 systemName,交由后续 OpenApiFilter 处理参数校验
            chain.doFilter(req, res);
            return;
        }

        GovernanceConfig config = configService.getByApiUrl(request.getRequestURI());
        if (config == null || config.getRateLimitQps() <= 0) {
            // 未配置限流,直接放行
            chain.doFilter(req, res);
            return;
        }

        RateLimiter limiter = limiters.computeIfAbsent(
                systemName + ":" + request.getRequestURI(),
                k -> RateLimiter.create(config.getRateLimitQps()));

        if (!limiter.tryAcquire()) {
            GatewayResponse.failure(response, 1008, "请求过于频繁,已被限流");
            return;
        }

        chain.doFilter(req, res);
    }

    /**
     * 提取 systemName:优先从 query 参数,其次从 body(需复用缓存的 wrapper)。
     */
    private String extractSystemName(HttpServletRequest request) {
        String name = request.getParameter("systemName");
        if (name == null && request instanceof CachedBodyRequestWrapper) {
            name = ((CachedBodyRequestWrapper) request).getParam("systemName");
        }
        return name;
    }

    @Override
    public void destroy() {
    }
}

6.5 熔断 Filter

java 复制代码
package com.example.gateway.filter;

import com.example.gateway.model.GovernanceConfig;
import com.example.gateway.service.GovernanceConfigService;
import com.example.gateway.support.GatewayResponse;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 熔断 Filter:失败率阈值熔断(计数窗口)。
 * 状态机:Closed -> Open -> Half-Open -> Closed/Open
 */
@WebFilter(urlPatterns = "/api/v1/*", filterName = "CircuitBreakerFilter")
public class CircuitBreakerFilter implements Filter {

    /** 熔断状态 */
    private enum State { CLOSED, OPEN, HALF_OPEN }

    /** key(apiUrl) -> 熔断器 */
    private final ConcurrentHashMap<String, Breaker> breakers = new ConcurrentHashMap<>();

    private GovernanceConfigService configService;

    @Override
    public void init(FilterConfig filterConfig) {
        configService = SpringContextHolder.getBean(GovernanceConfigService.class);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String apiUrl = request.getRequestURI();

        GovernanceConfig config = configService.getByApiUrl(apiUrl);
        if (config == null || config.getCircuitThreshold() <= 0) {
            chain.doFilter(req, res);
            return;
        }

        Breaker breaker = breakers.computeIfAbsent(apiUrl, k -> new Breaker(config));

        if (!breaker.allowRequest()) {
            // 熔断打开,快速失败(可结合降级返回兜底)
            GatewayResponse.failure(response, 99999, "服务暂不可用,请稍后重试");
            return;
        }

        try {
            chain.doFilter(req, res);
            breaker.onSuccess();
        } catch (Exception e) {
            breaker.onFailure();
            throw e;
        }
    }

    /**
     * 简化的熔断器实现。
     */
    private static class Breaker {
        private final GovernanceConfig config;
        private final AtomicReference<State> state = new AtomicReference<>(State.CLOSED);
        private final AtomicLong windowStart = new AtomicLong(System.currentTimeMillis());
        private final AtomicLong success = new AtomicLong();
        private final AtomicLong failure = new AtomicLong();
        private volatile long openTime;

        Breaker(GovernanceConfig config) {
            this.config = config;
        }

        boolean allowRequest() {
            State s = state.get();
            if (s == State.CLOSED) {
                return true;
            }
            if (s == State.OPEN) {
                // 超过半开探测间隔,进入半开
                if (System.currentTimeMillis() - openTime > config.getCircuitHalfOpenMs()) {
                    state.set(State.HALF_OPEN);
                    return true;
                }
                return false;
            }
            // HALF_OPEN:放行一个探测请求
            return true;
        }

        void onSuccess() {
            if (state.get() == State.HALF_OPEN) {
                state.set(State.CLOSED);
                resetWindow();
            } else {
                success.incrementAndGet();
            }
            checkWindow();
        }

        void onFailure() {
            if (state.get() == State.HALF_OPEN) {
                // 半开探测失败,重新熔断
                openCircuit();
                return;
            }
            failure.incrementAndGet();
            checkWindow();
        }

        private void checkWindow() {
            if (System.currentTimeMillis() - windowStart.get() > config.getCircuitWindowMs()) {
                resetWindow();
                return;
            }
            long total = success.get() + failure.get();
            if (total >= 10) { // 最小样本量
                double failRate = (double) failure.get() / total * 100;
                if (failRate >= config.getCircuitThreshold()) {
                    openCircuit();
                }
            }
        }

        private void openCircuit() {
            state.set(State.OPEN);
            openTime = System.currentTimeMillis();
        }

        private void resetWindow() {
            windowStart.set(System.currentTimeMillis());
            success.set(0);
            failure.set(0);
        }
    }

    @Override
    public void destroy() {
    }
}

6.6 降级 Filter

java 复制代码
package com.example.gateway.filter;

import com.example.gateway.model.GovernanceConfig;
import com.example.gateway.service.GovernanceConfigService;
import com.example.gateway.support.GatewayResponse;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 降级 Filter:捕获业务异常,返回兜底响应,避免异常堆栈泄露。
 * 位于 Filter 链最后,包裹 Controller 执行。
 */
@WebFilter(urlPatterns = "/api/v1/*", filterName = "FallbackFilter")
public class FallbackFilter implements Filter {

    private GovernanceConfigService configService;

    @Override
    public void init(FilterConfig filterConfig) {
        configService = SpringContextHolder.getBean(GovernanceConfigService.class);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        GovernanceConfig config = configService.getByApiUrl(request.getRequestURI());

        try {
            chain.doFilter(req, res);
        } catch (Exception e) {
            if (config != null && config.isDegradeEnabled()) {
                // 降级:返回兜底响应,不抛异常
                GatewayResponse.failure((HttpServletResponse) res, 99999, "服务降级中,请稍后重试");
            } else {
                throw e;
            }
        }
    }

    @Override
    public void destroy() {
    }
}

6.7 请求体缓存包装器(支持 body 可重复读)

java 复制代码
package com.example.gateway.filter;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;

/**
 * 缓存请求体,解决 body 只能读一次的问题。
 * 后续 Filter 复用同一个 wrapper,避免流被消费。
 */
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {

    private final String body;

    public CachedBodyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        BufferedReader reader = request.getReader();
        try (StringWriter writer = new StringWriter()) {
            char[] buf = new char[8192];
            int read;
            while ((read = reader.read(buf)) != -1) {
                writer.write(buf, 0, read);
            }
            this.body = writer.getBuffer().toString();
        }
    }

    public String getBody() {
        return body;
    }

    /** 从 JSON body 中提取参数 */
    public String getParam(String key) {
        if (body == null || body.isEmpty()) {
            return null;
        }
        JSONObject obj = JSON.parseObject(body);
        return obj == null ? null : obj.getString(key);
    }

    @Override
    public ServletInputStream getInputStream() {
        ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        return new ServletInputStream() {
            @Override public int read() { return bais.read(); }
            @Override public boolean isFinished() { return bais.available() == 0; }
            @Override public boolean isReady() { return true; }
            @Override public void setReadListener(ReadListener listener) { }
        };
    }
}

6.8 Spring 上下文持有器(Filter 获取 Bean)

java 复制代码
package com.example.gateway.filter;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 由于 Servlet Filter 无法直接 @Autowired,通过此持有器获取 Spring Bean。
 */
@Component
public class SpringContextHolder implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }
}

6.9 鉴权 Filter

java 复制代码
package com.example.gateway.filter;

import com.example.gateway.support.GatewayResponse;
import com.example.gateway.util.RsaEncryptUtils;
import com.example.gateway.dao.OpenApiMapper;
import com.example.gateway.dao.SysApiRelMapper;
import com.example.gateway.model.OpenApiDO;
import com.example.gateway.model.OpenApiVerifyDTO;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.interfaces.RSAPublicKey;

/**
 * OpenAPI 鉴权 Filter:身份认证 + 授权控制。
 * 位于限流、熔断之后,降级之前。
 */
@WebFilter(urlPatterns = "/api/v1/*", filterName = "OpenApiFilter")
public class OpenApiFilter implements Filter {

    private static final String PARAM_SIGN = "sign";
    private static final String PARAM_SYSTEM_NAME = "systemName";
    private static final int SIGN_SWITCH_OPEN = 1;
    private static final int GREY_SWITCH_OPEN = 1;

    private OpenApiMapper openApiMapper;
    private SysApiRelMapper sysApiRelMapper;

    @Override
    public void init(FilterConfig filterConfig) {
        openApiMapper = SpringContextHolder.getBean(OpenApiMapper.class);
        sysApiRelMapper = SpringContextHolder.getBean(SysApiRelMapper.class);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 1. 灰度判定:灰度 API 直接放行
        OpenApiDO openApiDO = openApiMapper.getByApiUrl(request.getRequestURI());
        if (openApiDO != null && GREY_SWITCH_OPEN == openApiDO.getGrey()) {
            chain.doFilter(req, res);
            return;
        }

        // 2. 提取 sign、systemName
        CachedBodyRequestWrapper wrapper = (request instanceof CachedBodyRequestWrapper)
                ? (CachedBodyRequestWrapper) request
                : null;

        String sign = request.getParameter(PARAM_SIGN);
        String systemName = request.getParameter(PARAM_SYSTEM_NAME);
        if (sign == null || systemName == null) {
            if (wrapper == null) {
                wrapper = new CachedBodyRequestWrapper(request);
                request = wrapper;
            }
            sign = wrapper.getParam(PARAM_SIGN);
            systemName = wrapper.getParam(PARAM_SYSTEM_NAME);
        }

        if (sign == null || systemName == null || sign.isEmpty() || systemName.isEmpty()) {
            GatewayResponse.failure(response, 1001, "sign、systemName 参数必填");
            return;
        }

        // 3. 授权校验:该 URL 是否授权给该系统
        OpenApiVerifyDTO verifyDTO = sysApiRelMapper.getByUrlAndSys(request.getRequestURI(), systemName);
        if (verifyDTO == null) {
            GatewayResponse.failure(response, 1002, "系统或 API 未登记");
            return;
        }

        // 4. 验签
        if (SIGN_SWITCH_OPEN == verifyDTO.getSignSwitch()) {
            boolean verifyResult;
            try {
                RSAPublicKey publicKey = RsaEncryptUtils.loadPublicKey(verifyDTO.getPublicKey());
                verifyResult = RsaEncryptUtils.verify(
                        systemName.getBytes(),
                        RsaEncryptUtils.hexToByteArray(sign),
                        publicKey);
            } catch (Exception e) {
                verifyResult = false;
            }
            if (!verifyResult) {
                GatewayResponse.failure(response, 2002, "API 鉴权未通过");
                return;
            }
        }

        // 5. 放行(传递缓存的 wrapper)
        chain.doFilter(wrapper != null ? wrapper : request, res);
    }

    @Override
    public void destroy() {
    }
}

6.10 数据模型(DO / DTO)

java 复制代码
package com.example.gateway.model;

import lombok.Data;

/** 开放的 API 表实体,对应 t_open_api */
@Data
public class OpenApiDO {
    private Long id;
    private String apiUrl;
    private String description;
    /** 灰度标记:0-否,1-是 */
    private Integer grey;
}

/** 第三方系统信息表实体,对应 t_third_system_info */
@Data
public class ThirdSystemInfoDO {
    private Long id;
    private String thirdSystemName;
    private String publicKey;
    private String privateKey;
}

/** API 与系统关系表实体,对应 t_sys_api_rel */
@Data
public class SysApiRelDO {
    private Long id;
    private Long thirdSystemId;
    private Long openApiId;
    /** 是否验签:0-否,1-是 */
    private Integer isSign;
}

/** 鉴权校验结果 DTO(由授权关系联表查询得到) */
@Data
public class OpenApiVerifyDTO {
    private Long id;
    /** 是否验签:0-否,1-是 */
    private Integer signSwitch;
    private String publicKey;
    private String privateKey;
}

6.11 RSA 加解密与签名工具类(核心)

java 复制代码
package com.example.gateway.util;

import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * RSA 签名/验签工具类。
 * 流程:生成密钥对 -> Base64 编码存储 -> 私钥签名(hex 密文) -> 公钥验签。
 */
public final class RsaEncryptUtils {

    private RsaEncryptUtils() {
        throw new IllegalAccessError("Utility class");
    }

    /** 生成 RSA 密钥对(1024 位) */
    public static KeyPair generateKeyPair() throws GeneralSecurityException {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(1024, new SecureRandom());
        return generator.genKeyPair();
    }

    /** 私钥签名,返回签名字节数组 */
    public static byte[] rsaSign(byte[] data, RSAPrivateKey privateKey) throws GeneralSecurityException {
        Signature signature = Signature.getInstance("SHA512withRSA");
        signature.initSign(privateKey);
        signature.update(data);
        return signature.sign();
    }

    /** 私钥签名,返回十六进制字符串 */
    public static String rsaSignToHex(byte[] data, RSAPrivateKey privateKey) throws GeneralSecurityException {
        return byteToHex(rsaSign(data, privateKey));
    }

    /** 公钥验签 */
    public static boolean verify(byte[] data, byte[] sign, RSAPublicKey publicKey) throws GeneralSecurityException {
        Signature signature = Signature.getInstance("SHA512withRSA");
        signature.initVerify(publicKey);
        signature.update(data);
        return signature.verify(sign);
    }

    /** 从 Base64 字符串加载公钥 */
    public static RSAPublicKey loadPublicKey(String publicKeyStr) throws GeneralSecurityException {
        byte[] buffer = Base64.getDecoder().decode(publicKeyStr);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
        return (RSAPublicKey) keyFactory.generatePublic(keySpec);
    }

    /** 从 Base64 字符串加载私钥 */
    public static RSAPrivateKey loadPrivateKey(String privateKeyStr) throws GeneralSecurityException {
        byte[] buffer = Base64.getDecoder().decode(privateKeyStr);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
        return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
    }

    /** byte[] 转十六进制字符串 */
    public static String byteToHex(byte[] data) {
        StringBuilder sb = new StringBuilder();
        for (byte b : data) {
            String hex = Integer.toHexString(0xFF & b);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    /** 十六进制字符串转 byte[] */
    public static byte[] hexToByteArray(String hex) {
        String s = hex;
        int len = s.length();
        byte[] result;
        if (len % 2 != 0) {
            len++;
            result = new byte[len / 2];
            s = "0" + s;
        } else {
            result = new byte[len / 2];
        }
        int j = 0;
        for (int i = 0; i < len; i += 2) {
            result[j] = (byte) Integer.parseInt(s.substring(i, i + 2), 16);
            j++;
        }
        return result;
    }
}

6.12 授权管理 Service(登记系统 / 生成密钥 / 授权绑定 / 密钥更新)

java 复制代码
package com.example.gateway.service;

import com.example.gateway.dao.ThirdSystemInfoMapper;
import com.example.gateway.dao.SysApiRelMapper;
import com.example.gateway.model.ThirdSystemInfoDO;
import com.example.gateway.util.RsaEncryptUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Base64;

/**
 * OpenAPI 授权管理服务。
 * 负责第三方系统登记、密钥对生成、授权关系绑定与密钥更新。
 */
@Service
public class OpenApiAuthService {

    @Resource
    private ThirdSystemInfoMapper thirdSystemInfoMapper;

    @Resource
    private SysApiRelMapper sysApiRelMapper;

    /**
     * 登记第三方系统,生成并保存密钥对。
     * 私钥需安全地提供给第三方系统(一次性返回)。
     */
    public String addThirdSystem(String thirdSystemName) throws GeneralSecurityException {
        // 1. 校验系统是否已登记
        ThirdSystemInfoDO existed = thirdSystemInfoMapper.getByName(thirdSystemName);
        if (existed != null) {
            throw new IllegalStateException("系统 " + thirdSystemName + " 已存在");
        }

        // 2. 生成 RSA 密钥对
        KeyPair keyPair = RsaEncryptUtils.generateKeyPair();
        String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
        String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());

        // 3. 保存
        ThirdSystemInfoDO thirdSystem = new ThirdSystemInfoDO();
        thirdSystem.setThirdSystemName(thirdSystemName);
        thirdSystem.setPublicKey(publicKey);
        thirdSystem.setPrivateKey(privateKey);
        thirdSystemInfoMapper.insert(thirdSystem);

        // 4. 返回私钥(交给第三方系统,服务端仅留存公钥用于验签)
        return privateKey;
    }

    /**
     * 为第三方系统授权某个 API。
     * @param isSign 是否开启验签(1-是,0-否)
     */
    public void grantApi(String thirdSystemName, String apiUrl, Integer isSign) {
        thirdSystemInfoMapper.getByName(thirdSystemName);
        // 具体绑定逻辑:查 thirdSystemId + openApiId,写入 t_sys_api_rel
        sysApiRelMapper.insertRel(thirdSystemName, apiUrl, isSign);
    }

    /**
     * 更新第三方系统密钥对(轮换密钥)。
     */
    @Transactional
    public String updateKeyPair(Long thirdSystemId) throws GeneralSecurityException {
        ThirdSystemInfoDO thirdSystem = thirdSystemInfoMapper.getById(thirdSystemId);
        if (thirdSystem == null) {
            throw new IllegalStateException("系统不存在");
        }
        KeyPair keyPair = RsaEncryptUtils.generateKeyPair();
        thirdSystem.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
        thirdSystem.setPrivateKey(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
        thirdSystemInfoMapper.updateKeyPair(thirdSystem);
        return thirdSystem.getPrivateKey();
    }

    /**
     * 删除第三方系统(同时删除其所有授权关系)。
     */
    @Transactional
    public void deleteSystem(Long thirdSystemId) {
        sysApiRelMapper.deleteByThirdSystemId(thirdSystemId);
        thirdSystemInfoMapper.deleteById(thirdSystemId);
    }
}

6.13 授权管理 Mapper(MyBatis 注解方式)

java 复制代码
package com.example.gateway.dao;

import com.example.gateway.model.OpenApiDO;
import com.example.gateway.model.OpenApiVerifyDTO;
import com.example.gateway.model.ThirdSystemInfoDO;
import org.apache.ibatis.annotations.*;

@Mapper
public interface OpenApiMapper {

    @Select("SELECT * FROM t_open_api WHERE api_url = #{apiUrl}")
    OpenApiDO getByApiUrl(@Param("apiUrl") String apiUrl);
}

@Mapper
public interface ThirdSystemInfoMapper {

    @Select("SELECT * FROM t_third_system_info WHERE third_system_name = #{name}")
    ThirdSystemInfoDO getByName(@Param("name") String name);

    @Select("SELECT * FROM t_third_system_info WHERE id = #{id}")
    ThirdSystemInfoDO getById(@Param("id") Long id);

    @Insert("INSERT INTO t_third_system_info(third_system_name, public_key, private_key) " +
            "VALUES(#{thirdSystemName}, #{publicKey}, #{privateKey})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(ThirdSystemInfoDO thirdSystem);

    @Update("UPDATE t_third_system_info SET public_key = #{publicKey}, private_key = #{privateKey} WHERE id = #{id}")
    int updateKeyPair(ThirdSystemInfoDO thirdSystem);

    @Delete("DELETE FROM t_third_system_info WHERE id = #{id}")
    int deleteById(@Param("id") Long id);
}

@Mapper
public interface SysApiRelMapper {

    /**
     * 联表查询:根据 API URL 和系统名,获取授权关系 + 公钥(用于验签)。
     */
    @Select("SELECT r.id, r.is_sign AS signSwitch, s.public_key AS publicKey, s.private_key AS privateKey " +
            "FROM t_sys_api_rel r " +
            "JOIN t_third_system_info s ON r.third_system_id = s.id " +
            "JOIN t_open_api a ON r.open_api_id = a.id " +
            "WHERE a.api_url = #{apiUrl} AND s.third_system_name = #{systemName}")
    OpenApiVerifyDTO getByUrlAndSys(@Param("apiUrl") String apiUrl, @Param("systemName") String systemName);

    /**
     * 插入授权关系。
     */
    @Insert("INSERT INTO t_sys_api_rel(third_system_id, open_api_id, is_sign) " +
            "SELECT s.id, a.id, #{isSign} FROM t_third_system_info s, t_open_api a " +
            "WHERE s.third_system_name = #{systemName} AND a.api_url = #{apiUrl}")
    int insertRel(@Param("systemName") String systemName, @Param("apiUrl") String apiUrl, @Param("isSign") Integer isSign);

    @Delete("DELETE FROM t_sys_api_rel WHERE third_system_id = #{thirdSystemId}")
    int deleteByThirdSystemId(@Param("thirdSystemId") Long thirdSystemId);
}

6.14 授权管理 Controller(管理入口)

java 复制代码
package com.example.gateway.controller;

import com.example.gateway.service.OpenApiAuthService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;

/**
 * OpenAPI 授权管理接口(后台管理使用,需管理权限认证)。
 * 提供系统登记、API 授权、密钥更新、系统删除等能力。
 */
@RestController
@RequestMapping("/api/v2/manager/openapi")
public class OpenApiAuthController {

    @Resource
    private OpenApiAuthService openApiAuthService;

    /**
     * 登记第三方系统,返回生成的私钥(一次性)。
     */
    @PostMapping("/system")
    public Map<String, Object> addSystem(@RequestParam String thirdSystemName) throws GeneralSecurityException {
        String privateKey = openApiAuthService.addThirdSystem(thirdSystemName);
        Map<String, Object> result = new HashMap<>();
        result.put("thirdSystemName", thirdSystemName);
        result.put("privateKey", privateKey);
        result.put("tip", "私钥仅本次返回,请安全保管并下发给第三方系统");
        return result;
    }

    /**
     * 为第三方系统授权某个 API。
     */
    @PostMapping("/grant")
    public Map<String, Object> grantApi(@RequestParam String thirdSystemName,
                                        @RequestParam String apiUrl,
                                        @RequestParam Integer isSign) {
        openApiAuthService.grantApi(thirdSystemName, apiUrl, isSign);
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "授权成功");
        return result;
    }

    /**
     * 更新第三方系统密钥对。
     */
    @PutMapping("/system/key")
    public Map<String, Object> updateKeyPair(@RequestParam Long thirdSystemId) throws GeneralSecurityException {
        String privateKey = openApiAuthService.updateKeyPair(thirdSystemId);
        Map<String, Object> result = new HashMap<>();
        result.put("privateKey", privateKey);
        return result;
    }

    /**
     * 删除第三方系统(同时删除授权关系)。
     */
    @DeleteMapping("/system/{thirdSystemId}")
    public Map<String, Object> deleteSystem(@PathVariable Long thirdSystemId) {
        openApiAuthService.deleteSystem(thirdSystemId);
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "删除成功");
        return result;
    }
}

七、Filter 链与配置接入

7.1 Filter 注册顺序

Servlet 规范中,@WebFilter 的注册顺序不可控,因此必须通过配置类显式注册并指定顺序:

java 复制代码
package com.example.gateway.config;

import com.example.gateway.filter.*;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 显式注册 Filter 并指定执行顺序。
 * 顺序:限流(1) → 熔断(2) → 鉴权(3) → 降级(4)
 */
@Configuration
public class GatewayFilterConfig {

    @Bean
    public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
        FilterRegistrationBean<RateLimitFilter> bean = new FilterRegistrationBean<>(new RateLimitFilter());
        bean.addUrlPatterns("/api/v1/*");
        bean.setOrder(1);
        return bean;
    }

    @Bean
    public FilterRegistrationBean<CircuitBreakerFilter> circuitBreakerFilter() {
        FilterRegistrationBean<CircuitBreakerFilter> bean = new FilterRegistrationBean<>(new CircuitBreakerFilter());
        bean.addUrlPatterns("/api/v1/*");
        bean.setOrder(2);
        return bean;
    }

    @Bean
    public FilterRegistrationBean<OpenApiFilter> openApiFilter() {
        FilterRegistrationBean<OpenApiFilter> bean = new FilterRegistrationBean<>(new OpenApiFilter());
        bean.addUrlPatterns("/api/v1/*");
        bean.setOrder(3);
        return bean;
    }

    @Bean
    public FilterRegistrationBean<FallbackFilter> fallbackFilter() {
        FilterRegistrationBean<FallbackFilter> bean = new FilterRegistrationBean<>(new FallbackFilter());
        bean.addUrlPatterns("/api/v1/*");
        bean.setOrder(4);
        return bean;
    }
}

注意:使用 FilterRegistrationBean 后,应移除各 Filter 类上的 @WebFilter 注解,避免重复注册。

7.2 需要注册为 Bean 的 Mapper

java 复制代码
package com.example.gateway.dao;

import com.example.gateway.model.OpenApiDO;
import com.example.gateway.model.OpenApiVerifyDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface OpenApiMapper {
    @Select("SELECT * FROM t_open_api WHERE api_url = #{apiUrl}")
    OpenApiDO getByApiUrl(@Param("apiUrl") String apiUrl);
}

@Mapper
public interface SysApiRelMapper {
    @Select("SELECT r.id, r.is_sign AS signSwitch, s.public_key AS publicKey, s.private_key AS privateKey " +
            "FROM t_sys_api_rel r " +
            "JOIN t_third_system_info s ON r.third_system_id = s.id " +
            "WHERE r.open_api_id = (SELECT id FROM t_open_api WHERE api_url = #{apiUrl}) " +
            "AND s.third_system_name = #{systemName}")
    OpenApiVerifyDTO getByUrlAndSys(@Param("apiUrl") String apiUrl, @Param("systemName") String systemName);
}

八、数据库脚本

sql 复制代码
-- 第三方系统信息表
CREATE TABLE t_third_system_info (
    id                BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    create_time       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    third_system_name VARCHAR(300) NOT NULL COMMENT '第三方系统简称',
    public_key        TEXT NOT NULL COMMENT '公钥',
    private_key       TEXT NOT NULL COMMENT '私钥'
) ENGINE = InnoDB COMMENT = '第三方系统信息表';

-- 开放的 API 表
CREATE TABLE t_open_api (
    id          BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    api_url     VARCHAR(500) UNIQUE NOT NULL COMMENT 'API 地址',
    description VARCHAR(500) COMMENT '简要描述',
    is_grey     TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '灰度标记 0-否 1-是'
) ENGINE = InnoDB COMMENT = '开放的 API 表';

-- API 与系统关系表
CREATE TABLE t_sys_api_rel (
    id              BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    create_time     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    third_system_id BIGINT UNSIGNED NOT NULL COMMENT '第三方系统 ID',
    open_api_id     BIGINT UNSIGNED NOT NULL COMMENT '开放 API ID',
    is_sign         TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否验签 0-否 1-是'
) ENGINE = InnoDB COMMENT = 'API 与系统关系表';

-- 流量治理配置表
CREATE TABLE t_api_governance (
    id                BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    create_time       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    open_api_id       BIGINT UNSIGNED NOT NULL COMMENT '关联 t_open_api.id',
    rate_limit_qps    INT NOT NULL DEFAULT 0 COMMENT '限流 QPS,0=不限流',
    circuit_threshold TINYINT NOT NULL DEFAULT 0 COMMENT '熔断失败率阈值%,0=不熔断',
    degrade_enabled   TINYINT NOT NULL DEFAULT 0 COMMENT '降级开关 0-关 1-开',
    UNIQUE KEY uk_governance_api (open_api_id)
) ENGINE = InnoDB COMMENT = 'API 流量治理配置表';

九、可移植性说明

9.1 移植步骤(迁移到新项目)

  1. 复制 filter、model、service、support 包下的类。
  2. 将 package com.example.gateway 替换为目标项目包名。
  3. 将 @WebFilter 注解移除,改用 GatewayFilterConfig 显式注册。
  4. 实现目标项目的 OpenApiMapper / SysApiRelMapper(或复用已有 DAO)。
  5. 执行数据库脚本(或复用已有 t_open_api 等表,增量加 t_api_governance)。
  6. 调整统一错误码,对齐目标项目的 WebReturnCode 体系。

9.2 可替换点

组件 默认实现 可替换为
配置读取 内存 Map DB / SCM / Apollo / Nacos
限流计数器 Guava RateLimiter(JVM) Redis Lua 令牌桶
熔断计数器 JVM 原子类 Redis / Resilience4j
签名算法 RSA HMAC / 其他

9.3 依赖清单

依赖 版本 是否必需
Spring Boot(Servlet) 2.x ✅
Guava 32.x(含 RateLimiter) 限流必需,可自实现替代
fastjson2 2.x 可选(JSON 解析)
Lombok 任意 可选(可手写 getter/setter)
MySQL / MyBatis 任意 可选(授权关系存储,可换内存)

十、演进路径(阶段二)

当出现以下信号时,将治理能力抽离为独立网关:

  • 调用方数量 / 流量持续增长,需要跨实例统一限流。
  • 需要灰度路由、多服务聚合、协议转换。
  • 后端服务拆分,需要统一流量入口。

抽离方式:阶段一的 Filter 逻辑(限流、熔断、鉴权、降级)与业务解耦充分,可直接平移为一个独立的网关应用(Spring Cloud Gateway / APISIX / 自研 Netty 服务),华佗 Web 只保留业务逻辑。

关键提前量:限流/熔断计数器已抽象为接口(见 5.4),单机切 Redis 时无需改动 Filter 代码,只需替换计数器实现。

相关推荐
Rain的Java大神之路1 小时前
「设计一个在线文档协同编辑系统」:从 WebSocket、OT/CRDT 到快照回放,一篇讲透这道面试顶流题
java·后端·面试
程序员梅雨1 小时前
【从零开始学习数据仓库 | 第三篇】: Flume——数据采集基石
java·大数据·数据仓库·后端·学习·flume
acd120091 小时前
域名解析缓存带来的隐性故障,很多排查都漏掉了这一层
java·运维·nginx
wuminyu1 小时前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++
真上帝的左手1 小时前
2. 编程语言-Java-Spring Boot-JAR
java·spring boot·jar
Jack_hsfs1 小时前
第6章Spring 管理的是对象,还是对象的说明书?
java·后端·spring
十二同学啊2 小时前
Python 工程化详解:项目结构、依赖管理、代码质量、测试与部署
java·开发语言·python
YIN_尹2 小时前
【git原理与使用】git基本操作及底层原理(一)
java·git·elasticsearch