网关统一入口实战:三层设防禁止服务直连,确保接口只能通过网关转发

发布时间:2026/10/24 7:09:57
网关统一入口实战:三层设防禁止服务直连,确保接口只能通过网关转发

1. 为什么需要网关统一入口

1.1 从一次线上事故说起

去年我们团队遇到一个挺典型的问题:某个内部服务上线了新版本,接口路径做了调整,但调用方没同步更新,结果一部分请求绕过了网关直接打到了服务实例上。因为服务本身没有做鉴权,那些请求带着伪造的用户标识就进来了,数据被越权读取了一部分。事后复盘,根因不是某个接口写错了,而是服务实例的网络位置对调用方是可见的——只要知道IP和端口,谁都能直接调。

这件事之后我们下决心做一件事:让所有外部流量必须经过网关,服务实例只接受来自网关的转发,任何绕过网关的直接调用一律拒绝。这就是标题里说的"gateway确保服务只能通过gateway转发,禁止直接调用接口访问"。

这个需求听起来简单,做起来涉及网络层、应用层、配置管理好几个层面。下面我把整个思路和落地过程拆开讲。

1.2 这个方案解决什么问题

先说清楚它到底防的是什么。很多人第一反应是"防外部攻击",其实不完全是。它主要防三类情况:

  • 内部服务被直接访问:服务实例暴露在可达网段里,调用方跳过网关直连,绕过了网关上的鉴权、限流、日志。
  • 鉴权逻辑被架空:网关做了统一的token校验、签名验证,但直连请求根本不走这一层,等于鉴权形同虚设。
  • 流量统计失真:网关是流量的统一观测点,绕过它意味着监控、计费、审计数据都不准。

所以这个方案的本质是收敛入口,把网关变成唯一的合法通道。它适合任何有多个后端服务、需要统一管控流量的架构,不管是微服务还是单体拆分后的过渡阶段。

1.3 整体思路:三层设防

我把它归纳成三层,从外到内依次收紧:

层级手段作用
网络层安全组/防火墙/网络策略限制服务实例只接受网关所在节点的流量
应用层请求头校验/来源标识服务自身识别请求是否来自网关
配置层服务注册与发现隔离让服务实例不直接对外暴露地址

三层不是必须全上,但网络层是底线,应用层是兜底,配置层是锦上添花。实际落地时我建议至少做前两层,因为单靠一层总有绕过的方法。

提示:不要指望只靠应用层校验就万事大吉。请求头是可以伪造的,如果网络层没限制,攻击者直接构造一个带正确标识的请求就能骗过服务。网络层限制才是真正的硬约束。

2. 网络层:让服务实例只认网关

2.1 安全组与防火墙的基本配置

网络层是最直接的手段。核心逻辑是:服务实例的入站规则里,只允许网关节点的IP或网段访问服务端口,其他一律拒绝。

以常见的云环境安全组为例,规则大概是这样:

# 假设网关节点所在网段是 10.0.1.0/24 # 服务实例监听 8080 端口 # 允许网关网段访问 入站规则: 协议TCP, 端口8080, 源 10.0.1.0/24, 动作 允许 # 拒绝其他所有 入站规则: 协议TCP, 端口8080, 源 0.0.0.0/0, 动作 拒绝

这里有个细节很多人会踩坑:规则的优先级。云厂商的安全组通常按规则顺序匹配,或者允许规则优先于拒绝规则。如果你把"拒绝所有"放在前面,那"允许网关"就永远不生效了。我建议先确认清楚所用平台的匹配逻辑,再决定规则顺序。

另一个坑是网段划分。如果网关和服务在同一个子网里,用网段限制就没意义了,因为同子网内互相访问本来就不受安全组约束(取决于具体实现)。这种情况要么把网关单独放到一个子网,要么用更细粒度的主机级防火墙。

2.2 容器环境下的网络策略

如果服务跑在容器编排平台里,安全组那套就不适用了,得用网络策略。以常见的Kubernetes环境为例,NetworkPolicy可以精确控制Pod之间的流量。

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: only-allow-gateway namespace: backend spec: podSelector: matchLabels: app: order-service policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: gateway ports: - protocol: TCP port: 8080

这段策略的意思是:order-service这个Pod只接受带app: gateway标签的Pod访问8080端口,其他任何来源的流量都被丢弃。

实测下来有几个注意点:

  • NetworkPolicy需要CNI插件支持。不是所有集群默认都启用,部署前先确认你的网络插件(比如Calico、Cilium)是否支持策略生效。
  • 默认拒绝要显式声明。如果只写了允许规则,没写policyTypes: Ingress,有些实现下默认还是全通。一定要把策略类型写全。
  • DNS解析别漏了。服务之间通常要通过服务名互相访问,如果策略把DNS流量也挡了,服务会解析失败。记得给kube-dns或coredns放行。

2.3 网络层方案的取舍

网络层最硬,但也不是没有代价。我总结了几点实际感受:

  • 运维复杂度上升:每次新增网关节点或调整网段,都要同步更新所有服务的规则,服务多了容易漏。
  • 调试变麻烦:开发同学本地想直连服务调试,会被挡住,需要额外开白名单或走跳板。
  • 不适合动态扩缩容:如果网关节点IP是动态的,规则维护会很痛苦,这时候更适合用标签选择器而不是IP。

所以网络层适合作为基础防线,但别指望它解决所有问题。它管的是"能不能连上",管不了"连上之后请求合不合法"。

3. 应用层:服务自身识别网关来源

3.1 请求头标识方案

网络层挡住了大部分直连,但如果网关和服务在同一网段,或者有人从网关节点上发起请求,网络层就失效了。这时候需要服务自己判断:这个请求是不是从网关转发过来的。

最常见的做法是网关在转发时注入一个特殊请求头,服务端校验这个头。

// 服务端拦截器伪代码 public boolean preHandle(HttpServletRequest request) { String gatewayFlag = request.getHeader("X-Gateway-Forwarded"); String secret = request.getHeader("X-Gateway-Secret"); if (!"true".equals(gatewayFlag)) { throw new UnauthorizedException("请求必须经过网关"); } if (!expectedSecret.equals(secret)) { throw new UnauthorizedException("网关标识无效"); } return true; }

网关侧配置注入:

# 网关路由配置示例 routes: - id: order-service uri: http://order-service:8080 filters: - AddRequestHeader=X-Gateway-Forwarded, true - AddRequestHeader=X-Gateway-Secret, ${GATEWAY_SECRET}

3.2 为什么用密钥而不只是布尔标识

有人会问,既然网络层已经限制了,应用层加个X-Gateway-Forwarded: true不就行了,为什么还要密钥?

因为请求头是可以伪造的。如果只校验一个固定的布尔值,任何能连上服务的人都能自己加上这个头。加上一个只有网关和服务知道的密钥,伪造难度就高多了。密钥通过环境变量注入,不写在代码里,也不进版本库。

注意:密钥方案的前提是密钥不泄露。如果密钥被硬编码在客户端或者日志里打印出来,那这层防护就白做了。密钥要定期轮换,轮换时网关和服务要能同时接受新旧两个密钥,避免切换瞬间请求全挂。

3.3 更严格的做法:签名校验

密钥方案已经能挡住大部分场景,但如果对安全性要求更高,可以用签名。网关转发时对请求体、时间戳、路径做一个HMAC签名,服务端用同样的密钥验签。

# 网关侧签名 import hmac, hashlib, time def sign_request(body, path, secret): timestamp = str(int(time.time())) message = f"{timestamp}{path}{body}" signature = hmac.new( secret.encode(), message.encode(), hashlib.sha256 ).hexdigest() return timestamp, signature

服务端验签时还要检查时间戳,超过一定时间窗口(比如5分钟)的请求直接拒绝,防止重放攻击。

这套方案的好处是请求内容被绑定,改任何一个字节签名都对不上。代价是网关和服务都要做加解密计算,有一定性能开销。实测在QPS几千的场景下,额外延迟在毫秒级,可以接受。

3.4 应用层方案的注意事项

应用层校验有几个容易忽略的点:

  • 健康检查要放行:服务的健康检查接口通常由编排平台直接调用,不经过网关。如果拦截器把健康检查也挡了,服务会被判定为不健康。记得给健康检查路径开白名单。
  • 内部调用要区分:如果服务之间还有内部RPC调用,这些调用可能也不经过网关。要么让内部调用也走网关,要么在拦截器里区分内外网来源。
  • 错误码要统一:被拦截的请求返回什么状态码?我建议用403而不是401,因为401会触发客户端的重新认证逻辑,而这里的问题是"来源不合法",不是"没登录"。

4. 配置层:让服务地址不对外暴露

4.1 服务注册与发现的隔离

前两层是从"访问控制"角度做的,配置层是从"信息隐藏"角度做的。核心思路是:让调用方根本不知道服务实例的真实地址,只知道网关地址。

在服务注册中心里,服务实例注册时可以选择只注册到内网命名空间,或者注册时带上元数据标记"仅网关可访问"。网关订阅这些服务做路由,而普通调用方拿不到这些实例列表。

# 服务注册元数据示例 service: name: order-service metadata: visibility: gateway-only gateway-route: /api/order/**

这样即使有人拿到了注册中心的访问权限,看到的也是"这个服务只对网关可见",而不是具体的IP端口。

4.2 内部DNS与外部DNS分离

另一个实用手段是DNS分离。内部服务用一套内部域名,只在集群内解析;对外只暴露网关的域名。调用方配置里写的是网关地址,根本接触不到服务实例地址。

这个方案在传统架构里很常见,在容器环境里对应的是Service和Ingress的区分:Service是集群内地址,Ingress是外部入口。只要确保外部流量只能通过Ingress进来,Service不对外暴露,就达到了目的。

4.3 配置层的局限

说实话,配置层更多是"减少暴露面",不是"强制阻断"。它让直连变得困难,但不能保证直连一定失败。所以它必须和前两层配合使用,单独用配置层是不够的。

我一般把配置层当作辅助手段:网络层是硬墙,应用层是门锁,配置层是把门藏起来。三者叠加,绕过网关的成本就非常高了。

5. 完整落地流程与实操记录

5.1 实施步骤拆解

把前面三层串起来,完整的落地流程大概是这样:

  1. 梳理服务清单:列出所有需要保护的后端服务,确认它们的监听端口、当前网络位置、调用方。
  2. 确认网关能力:检查网关是否支持请求头注入、签名、路由配置。主流网关(如Spring Cloud Gateway、Nginx、Envoy)都支持。
  3. 配置网络策略:按服务逐个配置安全组或NetworkPolicy,只放行网关来源。
  4. 网关注入标识:在网关路由配置里加上请求头注入或签名逻辑。
  5. 服务端加拦截器:在每个服务里加统一的拦截器,校验网关标识。
  6. 灰度验证:先在一个非核心服务上验证,确认正常流量通过、直连被拒。
  7. 全量推广:验证通过后逐个服务推广,同时监控错误率和延迟。
  8. 定期审计:定期检查是否有服务漏配,是否有新的直连路径出现。

5.2 灰度验证的关键指标

灰度阶段我重点看几个指标:

指标预期变化异常处理
网关转发成功率保持稳定下降说明网关配置有问题
服务直连请求数降为0不为0说明有调用方没切过来
服务P99延迟略有上升(加了一层校验)大幅上升说明拦截器有性能问题
403错误数先升后降持续升高说明有合法流量被误拦

特别说一下403错误。灰度初期一定会有一批403,因为有些调用方还在直连。这时候不要急着放宽规则,而是去排查这些调用方是谁,让他们切到网关。如果确实是合法调用被误拦,再针对性放行。

5.3 一个容易忽略的环节:网关自身的高可用

把网关变成唯一入口之后,网关就成了单点。如果网关挂了,所有服务都不可用。所以做这个方案的同时,必须保证网关本身是高可用的:多实例部署、负载均衡、健康检查、故障自动摘除。

我们当时的做法是网关至少3个实例,前面挂负载均衡,任何一个实例挂掉都不影响整体。同时网关的配置要做版本管理,出问题能快速回滚。

6. 常见问题与排查技巧

6.1 问题速查表

现象可能原因排查方向
所有请求都403拦截器密钥配置错误检查网关和服务端的密钥是否一致
部分请求403有调用方还在直连看403请求的来源IP,定位调用方
健康检查失败拦截器没放行健康检查路径检查白名单配置
延迟明显上升签名计算开销大评估是否降级为请求头校验
网关重启后全部失败密钥没持久化,重启后变了密钥用配置中心或环境变量管理
新服务上线后能被直连新服务没配网络策略把网络策略纳入服务上线检查清单

6.2 几个踩过的坑

坑一:密钥轮换导致大面积失败。我们第一次轮换密钥时,先改了网关,再改服务,中间有几分钟窗口期,网关用新密钥签名,服务还在用旧密钥验签,全部失败。后来改成网关同时接受新旧密钥,服务先更新,确认全部更新完再摘掉旧密钥。

坑二:内网调用被误伤。有个服务之间的内部调用没走网关,加了拦截器之后直接被挡。后来我们在拦截器里加了一个判断:来自内网特定网段的调用可以豁免,但要走单独的鉴权逻辑。这个豁免规则要非常谨慎,网段范围要尽可能小。

坑三:日志里打印了密钥。有次排查问题,开发同学把请求头全打进了日志,密钥就这么泄露了。后来我们在日志框架里加了脱敏规则,敏感请求头一律不打印。

6.3 独家避坑建议

  • 拦截器要可开关:留一个配置项能临时关闭校验,出问题时能快速降级,不至于整个服务不可用。
  • 监控要覆盖拦截率:把拦截器拦截的请求数做成监控指标,突然升高或降低都要告警。
  • 文档要同步更新:调用方接入文档里要明确写"必须通过网关调用",并给出网关地址和调用示例。
  • 定期做渗透测试:自己模拟直连请求,验证防护是否真的生效。别等出事了才发现某条路径没覆盖。

7. 方案的价值与边界

这套方案落地之后,我们最直观的感受是流量变得可观测、可管控了。以前排查问题要在好几个服务之间跳,现在网关是统一入口,请求链路、鉴权结果、限流情况都在一个地方能看到。

但它也不是万能的。它解决的是"入口收敛"问题,不解决"网关内部逻辑是否正确"的问题。如果网关自己的鉴权写错了,那所有请求都会带着错误的鉴权结果进来。所以网关本身的代码质量、配置管理、测试覆盖同样重要。

另外,这套方案对遗留系统不太友好。有些老服务架构上就不支持请求头注入,或者网络位置很难调整,改造起来成本很高。这种情况我建议先做网络层限制,应用层校验慢慢补,不要追求一步到位。

我个人在实际操作中的体会是:先做网络层,再做应用层,配置层随缘。网络层投入产出比最高,一天就能配完;应用层需要改代码,周期长一些;配置层更多是锦上添花,有精力再做。三层不必同时上,但网络层这层底线一定要守住。