非独立网关,属于历史版本限制下的优化方案
适用范围:任何基于 Servlet 容器(war 部署)的单体 Spring Boot 应用,需要对开放 API 进行「身份鉴权 + 授权控制 + 流量治理」
依赖:Spring Boot 2.x(Servlet 栈)、Guava(可选,用于限流)、MySQL(可选,用于授权关系存储)
一、设计目标与定位
在单体应用内部,为对外开放的 API(路径统一前缀,如 /api/v1/*)提供统一的:
- 身份认证(Authentication)------识别调用方是谁(RSA 签名验签)。
- 授权控制(Authorization)------判断调用方是否有权访问该 API。
- 流量治理(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 移植步骤(迁移到新项目)
- 复制
filter、model、service、support包下的类。 - 将
package com.example.gateway替换为目标项目包名。 - 将
@WebFilter注解移除,改用GatewayFilterConfig显式注册。 - 实现目标项目的
OpenApiMapper/SysApiRelMapper(或复用已有 DAO)。 - 执行数据库脚本(或复用已有
t_open_api等表,增量加t_api_governance)。 - 调整统一错误码,对齐目标项目的
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 代码,只需替换计数器实现。