网关已经统一鉴权,为什么微服务内部还要二次鉴权?
做微服务开发很多人都有个疑问既然网关 Gateway 已经统一拦截了请求校验了 Token 签名并把用户信息塞进 Header 传给下游服务了。为什么微服务内部的业务代码比如订单服务、会员服务里每个接口依然还要做一次鉴权和数据所有权校验网关把完关了微服务内部再鉴权这不是脱裤子放屁——多此一举吗这么想是因为多人觉得内网是安全的网关做好防护后内部就可以直接信任通信。但从实际安全和业务架构设计的角度来看网关的统一拦截与微服务内部的二次校验在职责分工上完全不同。网关和微服务到底怎么分工微服务架构的安全体系认证Authentication与授权Authorization是有着明确的分工。网关负责的是统一认证解决的是你是谁的问题。它是系统的流量入口主要负责全局的身份验证比如校验Token是否合法、是否过期。一旦通过网关就会解析出用户身份信息透传给下游。微服务内部负责的是业务授权解决的是你能干什么的问题。在身份确认后业务服务需要根据当前用户的身份校验他是否有权限操作特定的资源。比如校验当前用户是否有权修改这笔订单。这种涉及数据所有权、状态和规则的校验必须由具体的业务服务来做。其实这种职责划分也是微服务架构单一职责原则在安全领域的体现。内网就绝对安全有些传统公司内部默认外网是危险的内网是安全的。但在微服务架构中这种假设一旦成立就优点危险了。因为真实的内网里能出幺蛾子的地方其实非常多随手举三个高频的坑内网横向移动黑客通过网关某一个对外暴露、且存在严重漏洞的微服务拿到内网的一台服务器权限。如果内网微服务之间互相信任、接口裸奔黑客可以在这台机器上直接调用其他微服务的敏感接口比如http://order-service/delete?idxxx直接绕过网关。服务端请求伪造攻击 SSRF比如某个公开的微服务存在 SSRF 漏洞黑客诱导该服务向内网其他服务发送伪造请求。由于请求从内网发出你不做二次鉴权被请求的服务就会无条件执行。内鬼与运维误操作开发或者运维在内网调试可能直接跳过网关向微服务直接发起接口调用或者内部其他微服务写错了调用地址误删数据。所以现在微服务架构安全模型无一例外都是零信任模型。就是默认内网不安全每一次请求、每一个节点必须进行身份和权限的二次验证。业务授权网关干不了大部分也会想过“既然如此那我让网关把授权也干了不行吗网关拦截请求顺便去数据库查一下这个用户能不能操作这条数据。”千万别这么干这纯纯是自嗨式架构网关是整个业务集群的流量入口它的首要职责是快速转发和限流降级如果网关需要做细粒度的业务授权会产生很多问题。产生严重的业务耦合比如校验用户 A 能不能修改订单 B网关需要知道订单服务的业务逻辑必须去查询订单数据库判断订单 B 的 owner 是不是 A。 还要判断订单当前的状态如果是已发货可能就不能修改。这样一来所有微服务的业务逻辑和数据库查询都会堆积在网关层。网关从一个轻量级的流量转发器退化成一个臃肿的大单体服务微服务拆分的那就没意义了。网关性能下降网关通常基于Netty这类异步非阻塞框架像Spring Cloud Gateway就是如果为了鉴权在网关层发起大量的业务数据库查询会极大地占用网关的线程池和响应时间导致整体集群的入口并发量呈指数级下降。业内普遍的做法是什么明确了分工那在具体的业务中实践主流的架构一般采用以下方案网关层粗粒度认证与信息传递网关负责校验 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) publicclass OrderController { Autowired private OrderService orderService; // 1. 角色权限校验通过 Spring Security 校验当前用户是否拥有相应权限 PutMapping(/{orderId}) PreAuthorize(hasAuthority(ORDER_WRITE)) public ResponseEntityVoid 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. 安全防线如果订单拥有者和当前用户对不上直接拒绝访问 thrownew 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 双向 mTLSIstio等服务网格下微服务之间的所有网络通信默认加密且由Envoy代理进行身份互信验证从基础设施层彻底解决内网防伪问题。