大家好,我是小富~
做微服务开发,很多人都有个疑问:既然网关 Gateway 已经统一拦截了请求,校验了 Token 签名,并把用户信息塞进 Header 传给下游服务了。
为什么微服务内部的业务代码,比如订单服务、会员服务里,每个接口依然还要做一次鉴权和数据所有权校验?
网关把完关了,微服务内部再鉴权,这不是脱裤子放屁——多此一举吗?这么想是因为多人觉得内网是安全的,网关做好防护后内部就可以直接信任通信。
但从实际安全和业务架构设计的角度来看,网关的统一拦截与微服务内部的二次校验,在职责分工上完全不同。
网关和微服务到底怎么分工?
微服务架构的安全体系 认证(Authentication)与授权(Authorization) 是有着明确的分工。
网关负责的是统一认证,解决的是你是谁的问题。它是系统的流量入口,主要负责全局的身份验证,比如校验 Token 是否合法、是否过期。一旦通过网关就会解析出用户身份信息,透传给下游。
微服务内部负责的是业务授权,解决的是你能干什么的问题。在身份确认后,业务服务需要根据当前用户的身份,校验他是否有权限操作特定的资源。比如校验当前用户是否有权修改这笔订单。这种涉及数据所有权、状态和规则的校验,必须由具体的业务服务来做。
其实这种职责划分,也是微服务架构单一职责原则在安全领域的体现。
内网就绝对安全?
有些传统公司内部,默认外网是危险的,内网是安全的。但在微服务架构中,这种假设一旦成立,就优点危险了。
因为真实的内网里,能出幺蛾子的地方其实非常多,随手举三个高频的坑:
内网横向移动
黑客通过网关某一个对外暴露、且存在严重漏洞的微服务拿到内网的一台服务器权限。如果内网微服务之间互相信任、接口裸奔,黑客可以在这台机器上直接调用其他微服务的敏感接口,比如 http://order-service/delete?id=xxx,直接绕过网关。
服务端请求伪造攻击 SSRF
比如某个公开的微服务存在 SSRF 漏洞,黑客诱导该服务向内网其他服务发送伪造请求。由于请求从内网发出,你不做二次鉴权,被请求的服务就会无条件执行。
内鬼与运维误操作
开发或者运维在内网调试,可能直接跳过网关,向微服务直接发起接口调用,或者内部其他微服务写错了调用地址,误删数据。
所以现在微服务架构安全模型无一例外都是零信任模型。就是默认内网不安全,每一次请求、每一个节点,必须进行身份和权限的二次验证。
业务授权网关干不了
大部分也会想过:“既然如此,那我让网关把授权也干了不行吗?网关拦截请求,顺便去数据库查一下这个用户能不能操作这条数据。”
千万别这么干,这纯纯是自嗨式架构!
网关是整个业务集群的流量入口,它的首要职责是快速转发和限流降级,如果网关需要做细粒度的业务授权,会产生很多问题。
产生严重的业务耦合
比如校验用户 A 能不能修改订单 B,网关需要知道订单服务的业务逻辑,必须去查询订单数据库,判断订单 B 的 owner 是不是 A。 还要判断订单当前的状态,如果是已发货,可能就不能修改。
这样一来,所有微服务的业务逻辑和数据库查询都会堆积在网关层。网关从一个轻量级的流量转发器,退化成一个臃肿的大单体服务,微服务拆分的那就没意义了。
网关性能下降
网关通常基于 Netty 这类异步非阻塞框架,像 Spring Cloud Gateway 就是,如果为了鉴权在网关层发起大量的业务数据库查询,会极大地占用网关的线程池和响应时间,导致整体集群的入口并发量呈指数级下降。
图上要写的内容: 【错误设计:强耦合网关】 客户端 -> [API 网关] -> [查询订单数据库/校验业务逻辑] -> [订单库] (导致网关线程阻塞,拖垮流量入口)
【推荐设计:微服务自治】 客户端 -> [API 网关 (仅身份验证,透传 UserID)] -> [订单服务] -> [订单服务内部查询数据库 & 校验业务] -> [订单库]
业内普遍的做法是什么?
明确了分工,那在具体的业务中实践,主流的架构一般采用以下方案
网关层粗粒度认证与信息传递
网关负责校验 Token 的合法性,从 Token 中解析出最核心的用户上下文信息,如 userId, tenantId, roles,然后通过 HTTP Header,比如加密的 JSON,或者明文的头信息透传给下游。
X-User-Context: {"userId": "10001", "userName": "小富", "roles": ["ADMIN"]}
微服务层做细粒度授权与业务校验
微服务通常在入口处利用拦截器 Interceptor 或者 Filter,拦截请求头 X-User-Context,解析出用户身份并将其通过 MDC 绑定到线程上下文中,方便后续业务方法随拿随用。
在具体的业务方法或 Controller 上,我们就可以根据上下文进行二次校验。看一段标准的 Java 实现
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
// 1. 角色权限校验:通过 Spring Security 校验当前用户是否拥有相应权限
@PutMapping("/{orderId}")
@PreAuthorize("hasAuthority('ORDER_WRITE')")
public ResponseEntity<Void> updateOrder(@PathVariable Long orderId, @RequestBody OrderDto orderDto) {
// 2. 从当前线程 ThreadLocal 中获取网关传下来的用户 ID
Long currentUserId = UserContextHolder.getUserId();
// 3. 数据归属权校验:必须到数据库查一下,这笔订单是否真正属于当前用户
Order order = orderService.getById(orderId);
if (order == null) {
return ResponseEntity.notFound().build();
}
if (!order.getUserId().equals(currentUserId)) {
// 4. 安全防线:如果订单拥有者和当前用户对不上,直接拒绝访问
throw new AccessDeniedException("您无权修改该订单!");
}
orderService.update(orderId, orderDto);
return ResponseEntity.ok().build();
}
}
这段代码分拦截器处理身份解析与上下文绑定,Spring Security 校验了角色权限,业务代码校验了数据归属权,各司其职,网关完全不需要参与这部分的复杂逻辑。
内部服务调用和防伪怎么做?
这里紧接着就会衍生出一个非常现实的问题:如果是微服务内部相互调用,服务 A 用 Feign 调服务 B,下游服务怎么拿到这个用户上下文?总不能让服务 A 自己编一个吧?通常有两种解决方案
Feign 拦截器透传用户上下文
利用 Spring Cloud 的 RequestInterceptor,可以在 Feign 发起内部调用,自动把当前线程 ThreadLocal 中的 X-User-Context 请求头补上,实现身份信息的全链路透传。
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从当前线程 ThreadLocal 拿用户上下文
String userContextJson = UserContextHolder.getRawContext();
if (userContextJson != null) {
// 自动塞入 Feign 请求头中,往下游传递
template.header("X-User-Context", userContextJson);
}
}
}
内部信任与防伪校验
如果不做防御,恶意人员可以直接手动往微服务的接口塞一个 X-User-Context: {"userId": "1"} 绕过鉴权,对内网安全防伪有几种做法:
轻量级对称加密签名:网关生成透传 Header,用 HMAC-SHA256 共享密钥对 UserContext 进行签名并带上时间戳。微服务内部拿到后先用同样的密钥校验签名和是否过期。相比 RSA 非对称加密,对称加密速度快,高并发下的 CPU 损耗几乎可以忽略不计。
私网网段白名单限制:直接在微服务的网络或安全组层面进行限制,只允许网关 IP 段或者 K8s 内部 Pod 网段发起调用,凡是来自外网 IP 的直接请求一律拒绝。
Service Mesh 双向 mTLS:Istio 等服务网格下,微服务之间的所有网络通信默认加密,且由 Envoy 代理进行身份互信验证,从基础设施层彻底解决内网防伪问题。
图上要写的内容:
- 客户端 -(请求带 Token)-> [网关]
- [网关] -(用私钥对 UserContext 进行签名,生成 Signature)-> (透传带有 Header & Signature 的请求) -> [微服务 A]
- [微服务 A] -(用公钥验签 -> 提取 UserID)-> [微服务 A 业务处理]
- [微服务 A 业务处理] -(Feign 拦截器自动注入带有签名信息的 Header)-> [微服务 B]
- [微服务 B] -(用公钥校验签名 -> 通过后处理业务)-> 完成调用
说在最后
其实说白了,微服务的安全边界划分只有一句话:
网关防外,微服务防内。
微服务做二次鉴权和防伪校验确实麻烦,但要想系统在线上跑得稳当,这一步还真省不掉。好在这些代码也就写一次,封装进公共拦截器或者 Starter 之后,业务开发直接从上下文里拿用户 ID,平时写代码基本不受影响。
