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: - AddRequestHeaderX-Gateway-Forwarded, true - AddRequestHeaderX-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 实施步骤拆解把前面三层串起来完整的落地流程大概是这样梳理服务清单列出所有需要保护的后端服务确认它们的监听端口、当前网络位置、调用方。确认网关能力检查网关是否支持请求头注入、签名、路由配置。主流网关如Spring Cloud Gateway、Nginx、Envoy都支持。配置网络策略按服务逐个配置安全组或NetworkPolicy只放行网关来源。网关注入标识在网关路由配置里加上请求头注入或签名逻辑。服务端加拦截器在每个服务里加统一的拦截器校验网关标识。灰度验证先在一个非核心服务上验证确认正常流量通过、直连被拒。全量推广验证通过后逐个服务推广同时监控错误率和延迟。定期审计定期检查是否有服务漏配是否有新的直连路径出现。5.2 灰度验证的关键指标灰度阶段我重点看几个指标指标预期变化异常处理网关转发成功率保持稳定下降说明网关配置有问题服务直连请求数降为0不为0说明有调用方没切过来服务P99延迟略有上升加了一层校验大幅上升说明拦截器有性能问题403错误数先升后降持续升高说明有合法流量被误拦特别说一下403错误。灰度初期一定会有一批403因为有些调用方还在直连。这时候不要急着放宽规则而是去排查这些调用方是谁让他们切到网关。如果确实是合法调用被误拦再针对性放行。5.3 一个容易忽略的环节网关自身的高可用把网关变成唯一入口之后网关就成了单点。如果网关挂了所有服务都不可用。所以做这个方案的同时必须保证网关本身是高可用的多实例部署、负载均衡、健康检查、故障自动摘除。我们当时的做法是网关至少3个实例前面挂负载均衡任何一个实例挂掉都不影响整体。同时网关的配置要做版本管理出问题能快速回滚。6. 常见问题与排查技巧6.1 问题速查表现象可能原因排查方向所有请求都403拦截器密钥配置错误检查网关和服务端的密钥是否一致部分请求403有调用方还在直连看403请求的来源IP定位调用方健康检查失败拦截器没放行健康检查路径检查白名单配置延迟明显上升签名计算开销大评估是否降级为请求头校验网关重启后全部失败密钥没持久化重启后变了密钥用配置中心或环境变量管理新服务上线后能被直连新服务没配网络策略把网络策略纳入服务上线检查清单6.2 几个踩过的坑坑一密钥轮换导致大面积失败。我们第一次轮换密钥时先改了网关再改服务中间有几分钟窗口期网关用新密钥签名服务还在用旧密钥验签全部失败。后来改成网关同时接受新旧密钥服务先更新确认全部更新完再摘掉旧密钥。坑二内网调用被误伤。有个服务之间的内部调用没走网关加了拦截器之后直接被挡。后来我们在拦截器里加了一个判断来自内网特定网段的调用可以豁免但要走单独的鉴权逻辑。这个豁免规则要非常谨慎网段范围要尽可能小。坑三日志里打印了密钥。有次排查问题开发同学把请求头全打进了日志密钥就这么泄露了。后来我们在日志框架里加了脱敏规则敏感请求头一律不打印。6.3 独家避坑建议拦截器要可开关留一个配置项能临时关闭校验出问题时能快速降级不至于整个服务不可用。监控要覆盖拦截率把拦截器拦截的请求数做成监控指标突然升高或降低都要告警。文档要同步更新调用方接入文档里要明确写必须通过网关调用并给出网关地址和调用示例。定期做渗透测试自己模拟直连请求验证防护是否真的生效。别等出事了才发现某条路径没覆盖。7. 方案的价值与边界这套方案落地之后我们最直观的感受是流量变得可观测、可管控了。以前排查问题要在好几个服务之间跳现在网关是统一入口请求链路、鉴权结果、限流情况都在一个地方能看到。但它也不是万能的。它解决的是入口收敛问题不解决网关内部逻辑是否正确的问题。如果网关自己的鉴权写错了那所有请求都会带着错误的鉴权结果进来。所以网关本身的代码质量、配置管理、测试覆盖同样重要。另外这套方案对遗留系统不太友好。有些老服务架构上就不支持请求头注入或者网络位置很难调整改造起来成本很高。这种情况我建议先做网络层限制应用层校验慢慢补不要追求一步到位。我个人在实际操作中的体会是先做网络层再做应用层配置层随缘。网络层投入产出比最高一天就能配完应用层需要改代码周期长一些配置层更多是锦上添花有精力再做。三层不必同时上但网络层这层底线一定要守住。
阅读完成 · 觉得有帮助?