资讯详情

资讯详情

Feign调用安全实战:凭证透传、签名校验与敏感信息过滤

1. 从一个线上事故说起Feign调用为什么会翻车前阵子帮某公司排查一个线上问题现象很典型用户登录后访问某个页面前端请求网关一切正常但只要这个请求链路里涉及服务间的Feign调用就偶发出现“未授权”的错误。更诡异的是这个错误还不是每次必现而是隔一阵子冒出来一次导致前端的重试逻辑基本失效用户反复点按钮就是拿不到数据。追了半天最终定位到根因服务A通过Feign调用服务B的时候请求头里的用户凭证信息压根就没传过去。服务B拿不到用户身份自然就按“匿名请求”处理直接把请求拦在了鉴权层之外。定位到问题的那一刻团队里好几个人的第一反应都是一样的Feign调用居然还有这种坑这个案例其实特别典型。很多团队在微服务改造初期业务接口调通了、数据能返回了就觉得“服务调用已经OK了”。但真正的生产环境里服务和服务之间的调用不是把URL写对、参数传对就完事了——身份怎么传递、权限怎么校验、敏感信息会不会在链路里裸奔每一环都可能成为安全漏洞也可能成为线上故障的引爆点。这篇文章就围绕Feign服务调用中的安全问题进行完整拆解。我会从Feign调用的底层机制讲起说明安全风险产生的根源再给出经过生产验证的几种解决方案包括拦截器透传、签名校验、敏感信息过滤等最后把我在实际排查中踩过的坑和排查思路一并整理出来。无论你是刚接触微服务的开发者还是已经在生产环境里维护微服务体系的工程师这篇文章都能给你一些可以直接落地的参考。2. 先搞清楚Feign调用到底“安全”在哪里2.1 从一次“看起来正常”的调用说起先说一个最朴素的问题Feign调用不是走HTTP吗HTTP请求不是可以带Header吗为什么会出现上文中“凭证没传过去”的情况要理解这个问题得从Feign的调用链路说起。Feign是声明式的HTTP客户端你在接口上写一个注解框架就把接口变成一个可调用的代理对象。当业务代码调用这个代理对象的方法时Feign会根据接口定义、方法参数、注解配置把一次调用封装成一个HTTP请求然后通过底层的HTTP客户端发出去。但这里有一个关键点这个HTTP请求是Feign框架“自己组装”的它不会自动知道“当前业务线程里有一个用户请求用户请求里带了token你应该把这个token带上下游请求”。Feign的请求头默认只包含你显式指定的那些Header以及一些基础的Content-Type之类的头。用户请求上下文里的信息和Feign发起的下游请求默认是完全隔离的。所以问题的本质在于Feign帮我们屏蔽了HTTP细节但也屏蔽了“把上游上下文传递到下游”的细节。安全凭证的传递需要开发者自己处理。2.2 安全问题的三个层面我把Feign调用安全拆成三个层面看这样排查问题时思路会清晰很多。第一个层面是身份传递。用户登录后网关或者认证中心会签发一个凭证通常是一个token后续所有请求都带着这个凭证。服务间通过Feign相互调用时如果凭证没有传递过去下游服务就无法识别调用者是谁要么把请求拦截掉要么把请求当作匿名用户处理——两种情况对业务来说都是不可接受的。第二个层面是调用合法性。就算凭证传递机制做得很好还有一个问题谁能保证服务A发出的请求真的是服务A发出的微服务架构里服务间的调用常常走内网但内网不等于绝对安全。如果某个服务被攻破、或者某个内网调用入口被恶意扫描攻击者完全可以伪造一个Feign请求来冒充合法服务。要防住这一层光靠token透传不够还需要服务级别的身份认证和调用合法性校验。第三个层面是数据暴露。Feign调用时开发者为了方便经常图省事把整个用户对象、整个请求对象直接作为参数传给下游。这会导致一个问题下游服务明明只需要用户ID你却把用户的手机号、身份证号、密码哈希全部送了过去。调用链越长敏感信息的暴露面就越大。2.3 “内网调用不设防”是最常见的认知误区很多团队对Feign调用安全不重视核心原因是觉得“服务之间走的是内网外面的人进不来”。这个认知在早期单体架构时代问题不大但在微服务拆分彻底之后隐患会越来越明显。微服务架构下的内网实际上是一个“比较宽的边界”。服务实例是动态扩缩容的IP不固定调用链跨越多个服务每个环节都可能有日志记录、有缓存、有消息队列介入。哪怕网络层面隔离做得不错代码层面的漏洞、日志泄露、调试接口暴露都可能导致调用链路上的数据被非预期的角色获取。所以Feign调用的安全问题本质上不是“防外部攻击者”的问题而是“在服务间链路上建立明确信任边界”的问题。把每一跳调用都当成“可能被观察、被伪造、被滥用”的调用来对待安全设计才不会有大缺口。3. 方案选型解决Feign调用安全的三种主流思路3.1 方案一基于拦截器的凭证透传这是最基础、也最常用的方案。思路很简单用户请求进入服务A时把请求头里的凭证信息提取出来暂存到当前线程的上下文中当服务A通过Feign调用服务B时通过Feign的请求拦截器把暂存的凭证写入下游请求的Header。这个方案解决的是“身份传递”问题。实现上最关键的点是线程上下文的存取要正确。我们项目里早期用过一个基于ThreadLocal的上下文工具来传递凭证后来发现有个隐患如果Feign调用发生在异步线程比如用了Hystrix线程池隔离、或者CompletableFutureThreadLocal里的数据是拿不到的需要额外处理。这点在后面的“常见问题”部分我还会展开说。3.2 方案二服务间专用凭证Token透传有个特点下游服务拿到的是“用户的身份”。但有些场景下我们并不想让下游服务知道“当前用户是谁”只需要让它确认“这个请求确实来自合法上游”。这种场景就要用服务间专用凭证。做法是上游服务在下发Feign请求时额外带上一个服务间共享的凭证比如API Key、服务账号、App Secret下游服务校验这个凭证通过后再处理请求。这个凭证可以配置在配置中心定期轮换也可以做成服务账号体系的一部分每对服务间的凭证都不同。我在一个项目里见过一种更严格的实现上游服务调用下游时除了带API Key还会带一个对请求参数加签后的签名值下游服务用相同的密钥对参数重新签名、比对一致性。这样做的好处是即使请求被截获、API Key被泄露攻击者想篡改参数也不是一件容易的事。3.3 方案三全链路网关统一鉴权还有一种思路是把Feign调用安全的处理位置往上提。很多团队的微服务架构中已经部署了API网关用户请求统一经过网关。在这种架构下可以让“所有外部进来的请求”都在网关层完成身份认证内部服务之间默认不重复校验身份只做轻量级的来源校验。这种方案能减少很多重复开发但也有一个边界要划清楚网关只对“外部流量”负责无法拦截“服务间的直接调用”。如果服务A的某个接口因为是内网接口就没有鉴权而它又恰好被暴露到了外网比如运维配置失误、NAT规则错误安全防线就形同虚设。所以即便有网关服务间的调用校验也不能完全省掉。3.4 三种方案怎么选我的建议根据团队所处阶段和业务敏感程度我的选型建议如下表方案解决的核心问题适合场景实施成本风险点拦截器透传用户身份传递多数内部服务调用低上下文传递易出错异步场景需特殊处理服务间专用凭证调用来源合法性跨部门、跨系统的服务调用中密钥管理需跟上泄露后影响面大网关统一鉴权入口鉴权收敛有成熟网关体系的团队中高内网直连调用无法覆盖需配合方案一或二我的实践经验是线上系统至少要做到“凭证透传来源校验”两层大型系统再加一层“敏感信息过滤”。只做一层的话遇到真正的安全问题大概率会漏。透传解决的是“下游能不能认出来是谁”来源校验解决的是“这个调用是不是合法服务发出的”敏感信息过滤解决的是“不该传的数据不出服务边界”——三个问题都堵上Feign调用这环才算基本闭环。4. 实战环节从零写一个Feign安全调用方案4.1 第一步建立上下文传递机制先说最常用也最核心的一步。我们要确保用户请求里的凭证信息在Feign发起调用时能自动带到下游。在Spring Boot项目中我习惯用这样一组组件来干这个事一个上下文管理类负责暂存当前请求的凭证一个Feign拦截器负责把凭证写入下游请求。上下文管理类核心代码如下public class UserContext { private static final ThreadLocalUserInfo HOLDER new ThreadLocal(); public static void set(UserInfo userInfo) { HOLDER.set(userInfo); } public static UserInfo get() { return HOLDER.get(); } public static void clear() { HOLDER.remove(); } }注意两点一是这里的存放对象通常只需要放用户ID、token等必要信息别把整个用户大对象放进去减少内容占用和误用风险这个点的取舍逻辑我会在敏感信息那节细说二是必须在请求结束的Filter里调用clear()否则线程池复用会让上一个请求的用户信息串到下一个请求这是ThreadLocal最经典的坑。配套的Filter实现Component public class UserContextFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; String token httpRequest.getHeader(Authorization); if (StringUtils.hasText(token)) { // 解析token获得用户信息通常调用认证服务解析或本地解析JWT UserInfo userInfo parseToken(token); UserContext.set(userInfo); } try { chain.doFilter(request, response); } finally { UserContext.clear(); } } }4.2 第二步Feign拦截器透传凭证有了上下文下一步是写Feign的RequestInterceptor。这个拦截器会在Feign每次发请求前被调用我们在这里把上下文里的凭证取出来放到请求头里。Configuration public class FeignAuthConfig { Bean public RequestInterceptor userInfoRequestInterceptor() { return template - { UserInfo userInfo UserContext.get(); if (userInfo ! null StringUtils.hasText(userInfo.getToken())) { template.header(Authorization, userInfo.getToken()); template.header(X-User-Id, userInfo.getUserId()); } }; } }这里的逻辑很好理解如果当前上下文里存在用户信息就把token和用户ID加到Feign请求的Header里。下游服务如果有权限框架直接就能识别出调用者身份。还有一点值得做把X-User-Id带上。原因是很多下游服务并不需要解析token才能拿到用户ID直接读Header即可。这样既省去了token解析的成本也让调用链更简洁。不过要记住一个原则下游服务不能只信任X-User-Id这种可伪造头部涉及敏感操作时必须验证token的真实有效性。X-User-Id是用于业务处理的便利值不是安全边界。4.3 第三步给Feign请求加上来源校验token透传做完后就要处理“调用是不是合法服务发出的”这个问题了。处理方案上我用得比较顺手的是Feign请求拦截器叠加“服务凭证”方案。具体来说每对调用关系配置一组凭证上游调用下游时从配置中心读取凭证加到Feign请求的指定Header里下游服务收到请求后通过拦截器校验这个Header值是否正确。加在Feign拦截器里的代码很简单Bean public RequestInterceptor serviceAuthInterceptor() { return template - { String serviceSecret serviceAuthProperties.getSecret(); if (StringUtils.hasText(serviceSecret)) { template.header(X-Service-Secret, serviceSecret); } }; }下游服务侧的校验通常用Spring的HandlerInterceptor或Filter实现。收到请求后先检查Header值是否等于配置值不等就直接返回401。这里最关键的工程决策是密钥怎么管理。千万别把密钥hardcode在代码里我见过把服务密钥直接写在常量类里的代码一泄露整个内网的服务间调用凭证全废了。正确的做法是放到配置中心按环境隔离定期轮换。如果条件允许甚至可以给每对服务调用关系分配不同的密钥一把钥匙只开一扇门暴露之后也只影响一条调用链路。4.4 第四步对敏感信息做显式过滤凭证传递和来源校验搞定后还有一个容易被忽视的问题Feign调用时接口参数里带了不该带的敏感数据。举个我踩过的例子某个查询接口上游把整个用户对象作为参数传给下游用户对象里除了ID还有手机号、邮箱、身份证号。下游服务其实只需要用户ID但因为这个“顺手传对象”的设计下游的日志里就把手机号和邮箱完整打印出来了。后来日志系统被安全扫描发现存在未授权访问风险这批数据等于变相泄露了。解决这个问题我从三方面入手第一Feign接口的参数尽量用轻量DTO。不要直接把领域实体类当Feign参数传单独定义Feign接口专用的请求/响应对象只包含必要字段。例如调用用户服务时只传userId不传整个用户实体。第二调用链路的日志里禁止打印完整敏感字段。在日志配置层面给手机号、邮箱、身份证这类字段做脱敏处理或者干脆在Feign的接口实现里就不允许日志输出这些字段。第三敏感数据的传递走专门通道。某些业务场景下下游确实需要用户的手机号或身份证做业务处理这种场景就不建议在Feign调用的参数里裸传而是通过加密通道或者数据服务单独获取。核心思路是调用链上的每个服务只拿它完成业务所必需的最小数据集合。5. 实际排查Feign安全相关的四个典型坑5.1 坑一ServiceA本地调试正常链路一长就“失联”典型表现本机测试时Feign调用成功但部署到环境里请求经过网关再到服务A再到服务B底部状态就变成了401或403。查Feign日志发现下游收到的请求头里没有Authorization字段。根因通常是网关层用Ribbon或负载均衡器做了转发后把原始请求头里的信息清掉了或改名了而服务A在接收请求时又没有正确解析到用户信息。还有一种情况是上游服务用了内部跳转forward跳转后的请求不再是原始请求拦截器里解析不到Header。排查建议在下游服务的入口处把关键Header的值打印出来先确认Header到底有没有传到。没有的话往上游逐级查看到哪一跳丢的。有的话再确认解析逻辑有没有问题比如大小写是不是被改写了。这个排查链路我走通过很多次核心思路就是“沿着请求头走一遍”。5.2 坑二异步线程里拿不到用户上下文场景服务A的某个接口里用线程池异步执行了一个任务任务里通过Feign调用了服务B。结果发现这些异步调用的下游请求里没有任何用户凭证。原因之前提过Feign拦截器拿的是当前线程的UserContext而异步任务执行在线程池的某个工作线程里。当任务从提交线程切换到工作线程时ThreadLocal不会自动跟随。解决方案有两类一类是用TransmittableThreadLocal配合线程池装饰器让上下文跟着任务走另一类是异步任务开始时先把主线程里的用户上下文取出来并且手动传给子线程。两类方案里我更推荐前者因为它在代码层面侵入最小线程池复用也不会产生上下文串扰。5.3 坑三Feign启用服务发现后目标服务的路由变化导致请求打到错误实例这个坑表面上看和“安全”没关系实际影响特别大。当Feign配合服务发现如Nacos做负载均衡时如果某个服务实例下线不及时或者路由规则配置有问题请求可能被转发到一个不存在的实例上导致连接超时。更严重的情况是请求打到错误实例后重试机制会自动触发而重试时Feign拦截器默认会重新执行如果拦截器里依赖了某些只初始化一次的资源比如签名时间戳重试就可能造成请求内容不一致让下游校验不通过。这个问题的根治法则是把Feign的负载均衡规则和重试机制配置清楚最好在测试环境完整验证一遍超时与重试场景。签名类校验设计时时间戳要允许一定的偏移量避免重试即失败。5.4 坑四Feign服务配置了超时时间安全校验机制反而导致调用变慢这个坑是这样的下游服务为了做安全校验在校验逻辑里调用了远程认证服务换取用户信息每次Feign请求进来都多了一次远程调用。下游服务自己设置的Feign超时时间是2秒远程认证服务平均响应1.8秒加在一起刚好卡在超时边缘。线上流量一波动认证服务响应稍慢上游就大量超时报错。排查下来挺好笑的安全意识很强安全机制本身也OK但因为没评估性能开销安全机制反而变成拖垮调用的元凶。建议是安全校验逻辑里如果有远程调用要做本地缓存至少要设置合理的缓存过期时间。JWT类的凭证尽量本地解析不必每次都访问认证服务换取新token。签名校验尽量利用本地配置的密钥计算不要设计成每次都要远程获取公钥的模式当然公钥缓存策略要做好否则密钥轮换时会出问题。6. 写代码时的四个小习惯这段算是我个人在多个项目里总结出来的经验。细节看起来不起眼但影响都在线上。习惯一Feign接口的定义用专门的包和类别和业务接口混在一起。单独管理的好处是安全策略、日志策略可以统一加排查问题时候不用东翻西找。习惯二Feign调用设置合理的超时时间不要全部沿用默认值。默认值往往偏大或者偏小一旦下游服务变慢超时设置会放大故障影响。配置超时的时候顺便想想重试机制接口幂等性确认过才开重试否则重试可能造成重复下单、重复扣款。习惯三Feign的日志级别和生产环境区分开。开发环境可以开Full级别看请求和响应详情生产环境最多开Basic避免请求参数、响应数据落到日志文件里成为信息泄露的隐患。日志脱敏规则要前置不要等出了事再紧急处理。习惯四安全校验失败的异常要区别对待。配置好全局异常处理让安全校验失败、参数校验失败、业务校验失败有不同错误码这样排查问题时看一眼错误码就能定位是哪一层的校验挡住了请求能从根上提升排查效率也让下游系统更容易理解失败原因。7. 我最后想说的Feign调用安全这个问题看起来是个“技术难点”但本质上是个“工程意识”问题。很多坑不是踩不了而是没想过这里会有坑。我自己的体会是在做服务间调用的时候一定要有一个预设——每个通过Feign调用出去的请求经过的每一条链路、每一层日志、每一级缓存都可能有被观察的风险。代码安全数据安全链路安全三块都得照顾到缺一块都有可能出问题。如果你团队里现在正要做Feign的安全加固我的建议很简单先做到“凭证透传来源校验”两层这就已经能挡住绝大多数常见的生产问题。等链路复杂了、团队规模大了再逐步上更完整的签名机制、敏感数据过滤和服务间细粒度鉴权一把一把把口子补齐。这套思路我在几个项目里反复用过每一步都不复杂但合在一起就能让服务间的调用链路稳很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →