尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微服务认证授权实战:从Token到JWT防坑指南与安全加固方案

微服务认证授权实战:从Token到JWT防坑指南与安全加固方案 微服务拆得越细认证授权这个口子就越容易漏风。我做过好几个从单体拆到微服务的项目每次拆完之后第一个要重新设计的不是业务接口而是用户到底是谁、他能干什么。这个问题的答案一旦没想清楚后面所有服务之间的调用都会变成信任裸奔。今天我把这几年在微服务认证授权上踩过的坑和补上的洞整理成一篇可以照着排查和落地的实操总结重点放在真实环境里最容易出问题的几个环节以及对应的防护手段。这套内容适合谁看如果你的系统正在做微服务改造或者已经在用网关统一鉴权但总觉得哪里不踏实又或者刚接手一个微服务项目需要做安全巡检那这篇文章应该能帮你省下不少弯路。我会从认证授权的基本模型讲起逐步拆解Token生命周期、JWT使用陷阱、网关鉴权粒度、配置中心敏感信息保护这些核心环节最后附上高频问题的排查记录。1. 微服务认证授权的基本盘为什么传统Session方案撑不住了1.1 从单体到微服务认证授权的核心矛盾变了单体时代做登录态很简单用户登录后服务端存一份Session给浏览器种一个Cookie后续请求带着Cookie来服务端查一下Session就知道是谁了。这个模型在单个进程里跑得非常顺因为Session就存在本地内存里查询只是一次哈希查找。到了微服务架构下问题立刻变得尖锐。用户的一个请求到达网关后可能要被转发给订单服务、库存服务、用户服务三个节点处理每个服务都需要知道当前操作者是谁。如果每个服务都各自存一份Session那用户得登录三次如果所有服务共享一份Session存储那这个存储就会成为整个系统的高频访问点还要处理Session同步、过期策略、序列化兼容一堆问题。更麻烦的是服务之间的内部调用往往没有经过用户请求链路是A服务主动去调B服务的接口这种场景下压根不存在用户的Cookie和Session认证的对象不再是用户而是另一个服务。所以微服务架构下认证授权的设计思路必须从服务端记住我转变到请求自带身份。每个请求到达服务端时服务端不再依赖内存状态而是直接从请求携带的凭证中解析出调用者身份和权限范围。这个转变是所有后续方案的地基。1.2 主流的微服务认证授权模型Token成了事实标准当前主流做法是围绕Token来构建认证授权模型。用户登录成功后认证服务签发一个Token返回给客户端客户端在后续请求的Header里带上这个Token网关或各服务验证Token的合法性和有效期从中提取用户身份。这个模型的好处很明显无状态服务节点可以随便扩缩容不需要同步会话数据。认证逻辑可以收敛到网关或独立的认证服务业务服务不需要感知具体认证流程。Token本身可以携带用户基本信息、角色权限等声明减少下游服务的查询压力。Token不是只有一种形态。JWT是结构化自包含的服务端验签后就能读取声明Opaque Token是随机字符串服务端需要调用认证服务或查询存储才能得知其含义此外还有短期的Access Token配合长期的Refresh Token组合使用。选型时要结合自己的业务场景权衡我在后面会详细介绍。认证和授权在这个模型下也分成了两个层次。认证解决你是谁的问题通常由网关或认证服务统一处理授权解决你能干什么的问题可以粗粒度放在网关层细粒度的资源级权限控制则需要下沉到各个业务服务内部实现。很多团队只做了认证层以为网关验一下Token就万事大吉结果业务接口里的越权漏洞一个接一个这个教训后文会展开讲。2. 真实环境里最常见的四类认证授权漏洞2.1 Token泄露与重放拿到Token就等于拿到账号微服务架构下Token就是用户的通行证谁拿着Token谁就是用户本人。Token一旦泄露攻击者可以在有效期内冒充用户做任何事。我在实际项目里见到过的泄露途径主要有几类第一类是前端存储不当。有人为了方便直接把Token放localStorage里一旦页面被注入恶意脚本Token就能被直接读取。第二类是日志打印泄露网关层或业务服务在打印请求日志时把Authorization Header整个打印出来日志系统一旦被拖库Token就全量暴露。第三类是Token通过URL参数传递浏览器历史记录、反向代理日志、Referer头都会把Token带出去。第四类是第三方脚本加载页面里嵌入的统计脚本、客服脚本如果不可信也能窃取当前页面的存储内容。Token重放则是另一个维度的风险。攻击者不需要破解Token的加密算法只要拿到一个有效Token在过期前反复使用即可。尤其在某些业务场景下同一Token被多个服务验证只要有一个服务的校验逻辑不严Token就能被滥用。防护上除了后文会提到的HTTPS、存储策略、日志脱敏还需要从业务层面做风险控制。特别敏感的操作要加二次校验比如支付、改密、解绑手机号这些场景不应该仅凭一个Token就放行而是要结合短信验证码、支付密码、人脸识别等额外因子。Token的过期时间也要控制好过期时间越长重放攻击的窗口越大。2.2 JWT的三大经典误用算法混淆、密钥硬编码、声明过度信任JWT因为自包含、跨语言、无状态这些特性成了微服务里最常见的Token载体。但它也是被误用最多的技术我在代码评审里几乎每次都能挑出问题。算法混淆攻击是最经典的JWT漏洞。JWT的Header里有alg字段服务端验签时直接用这个字段决定用哪种算法。攻击者把alg改成none然后把签名部分去掉或者随便填如果服务端没有校验算法白名单就会把伪造的Token当作合法Token接受。还有个变体是RS256转HS256攻击服务端如果用RSA公钥验签攻击者把alg改成HS256服务端可能就会用RSA公钥字符串当作HMAC密钥来验证签名而公钥通常是公开的攻击者就能自己伪造签名。密钥硬编码是另一个高频问题。有人把JWT签名密钥直接写在代码里或者写死在配置文件里提交到Git仓库。我记得有个项目整改时用脚本扫了一下代码仓库历史记录密码、密钥、Token密钥全在里面躺着这种项目一旦代码泄露整个认证体系就形同虚设。声明过度信任是指业务服务拿到JWT后直接信任里面声明的用户角色、权限数据而不做任何核对。JWT毕竟是服务端签发的正常情况下里面的数据可信但问题是用户信息是会变的用户被降权了旧Token里还带着管理员角色在Token过期前用户还是能持有旧权限。还有一种情况用户信息存到了JWT的声明里一旦用户信息量大Token体积膨胀反而增加了泄露面。2.3 网关鉴权与业务鉴权脱节只挡了门口没管住房间微服务架构通常会在网关层做统一鉴权这本身是好的设计但问题在于很多人把网关鉴权当成了唯一的鉴权手段。我在一个项目里看到过这样的情况网关校验收到了Token而且没有过期就直接把请求转发到下游服务下游服务既不验证Token签名也不做任何权限判断。表面上看起来认证流程是通的实际上只要伪造一个合法的Token或者从别处盗用一个Token所有业务接口都畅通无阻。网关鉴权是粗粒度的它只能判断这个用户是不是合法用户但判断不了这个用户有没有权限操作这个订单。典型的越权场景就是水平越权和垂直越权。水平越权指用户A访问用户B的资源比如订单服务提供了查询订单详情的接口参数是订单号如果用户A拿到了用户B的订单号接口没校验订单归属就能看到别人的订单。垂直越权指低权限用户调用高权限接口比如普通用户直接调用管理员删除接口网关只判断了Token有效却没判断角色是否匹配。网关鉴权和业务鉴权必须协同工作。网关负责认证、基础安全防护、粗粒度授权业务服务必须自己再做一次细粒度的资源属主校验和角色权限校验。绝对不能因为网关做了鉴权业务服务就默认所有请求都是安全的。2.4 配置中心与网关暴露面Nacos、Knife4j这些基础设施藏着的雷热词里频繁出现Nacos和Knife4j这两样东西在微服务体系里太常见了但也经常成为安全短板。Nacos作为服务发现和配置中心承载着微服务架构里的核心元数据。如果Nacos的控制台和管理接口暴露在公网而且没有做认证改造攻击者可以直接访问控制台查看所有服务的注册信息、配置内容甚至修改配置在配置里注入恶意内容比如修改了数据源地址整个系统就会把数据写到攻击者的数据库里。配置中心里的配置往往包含数据库密码、Redis密码、各种密钥一旦泄露就是全量敏感信息泄露。Knife4j是接口文档增强工具开发阶段确实好用能自动生成在线调试页面。问题在于很多人上线时忘了关闭或做权限控制接口文档直接裸奔在公网。攻击者可以从文档里看到系统的完整API列表、参数结构、请求示例相当于拿到了系统接口地图再结合具体的漏洞点定向打击比盲打效率高得多。这两类问题的本质都是开发便利性与生产安全之间没有做好隔离。开发环境做得很便利但生产环境的暴露面必须收敛这是基础设施安全的基本原则。3. 防护措施落地方案从网关到服务的层层设防3.1 第一道防线网关层的统一认证与鉴权设计网关是所有外部请求进入系统的唯一入口这个位置适合做统一的安全校验。我在项目中通常会在网关层做以下几件事Token格式校验和签名验证识别Token是否为本系统签发。Token有效期校验过期Token直接拒绝。黑白名单校验被拉黑的用户或IP在网关层拦截。基础限流和防重放如时间戳校验、随机数缓存等。Spring Cloud Gateway是目前比较主流的网关选型结合Spring Security的认证过滤器可以实现一个比较完整的统一认证组件。核心思路是定义全局过滤器在过滤器里判断当前请求路径是否需要认证如果需要则从请求头提取Token并验证。Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Resource private JwtTokenProvider tokenProvider; private final ListString whiteList Arrays.asList( /api/auth/login, /api/auth/register, /api/auth/captcha ); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); if (whiteList.contains(path)) { return chain.filter(exchange); } String token resolveToken(exchange.getRequest()); if (token null || !tokenProvider.validateToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } String userId tokenProvider.getUserIdFromToken(token); ServerHttpRequest mutatedRequest exchange.getRequest().mutate() .header(X-User-Id, userId) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } Override public int getOrder() { return -100; } }这个过滤器把认证通过的userId放到了请求头里传给下游服务下游服务可以直接读取避免下游重复解析Token。但要注意下游服务不能无条件信任这个请求头因为内部服务之间的调用也可能被绕过后文会讲内部服务调用的安全方案。3.2 JWT的正确使用姿势算法白名单、密钥管理、声明精简JWT本身不是不安全的不安全的是使用方法。我在项目中总结了一套相对稳妥的JWT使用规范分享给大家参考。签名算法签发时统一用RS256非对称加密验签时必须校验alg字段是否在允许列表中。非对称加密的好处是签名密钥只保存在认证服务里其他业务服务只需要保存公钥即使某个业务服务被攻破攻击者也拿不到签名私钥无法伪造Token。// 解析JWT时强制指定算法防止算法混淆攻击 JwtParser parser Jwts.parserBuilder() .setSigningKey(publicKey) // 关键不为alg字段提供自动探测能力 .build();密钥管理生产环境的签名私钥必须放在专门的密钥管理服务中比如Vault或者至少使用环境变量注入严禁写死在代码和配置文件里。密钥要有轮换机制一旦怀疑泄露要能立刻吊销并重新签发。JWT不像Session那样能在服务端主动失效所以密钥轮换是应对Token泛滥的重要手段。声明设计JWT里只放必要的信息userId、登录时间、Token类型Access还是Refresh就够了。角色和权限信息尽量不放或者放一个版本号业务服务发现权限版本号变化时就主动重新加载权限数据。这样既能避免Token体积膨胀又能避免声明数据陈旧。过期策略Access Token的过期时间建议控制在15分钟到2小时之间Refresh Token可以放宽到7天到30天。Refresh Token一般存储在服务端的支持主动吊销客户端用Refresh Token换新Access Token时认证服务可以检查用户状态发现用户被禁用或改密后立即拒绝。3.3 业务服务的自我保护细粒度权限校验怎么做网关层的鉴权解决了是否合法用户的问题但是否有权操作这个资源必须由业务服务自己确认。我会在业务服务里做两层校验第一层是接口级别的权限校验。每个敏感接口都打了权限注解标明需要的角色或权限码。框架层面可以借助Spring Security的PreAuthorize注解运维人员可以在权限系统里给角色分配接口权限运行时由框架统一拦截校验。RestController RequestMapping(/api/order) public class OrderController { PreAuthorize(hasRole(ADMIN)) DeleteMapping(/{orderId}) public ResultVoid deleteOrder(PathVariable Long orderId) { // 仅管理员可操作 } GetMapping(/{orderId}) public ResultOrderVO getOrder(PathVariable Long orderId) { // 普通用户可访问但必须是本人订单 Long currentUserId SecurityUtils.getUserId(); OrderVO order orderService.getOrderById(orderId); if (!order.getUserId().equals(currentUserId)) { throw new BizException(无权访问该订单); } return Result.success(order); } }第二层是数据级别的归属校验。水平越权的核心就是缺少这一层校验查询或操作资源时必须将资源属主与当前用户比对。这个逻辑要写进业务代码无法靠中间件自动解决所以唯一可靠的办法是在代码评审和安全测试时重点关注涉及按ID查询按ID更新的接口确认都做了属主校验。有些团队会引入ABAC基于属性的权限控制模型把用户属性、资源属性、环境条件组合成策略实现更精细的动态权限判断。这个模型灵活但实现成本高适合权限规则复杂的大型系统。中小型项目从RBAC起步更务实等确实出现了RBAC覆盖不了的规则再引入ABAC。3.4 基础设施加固Nacos安全配置与Knife4j环境隔离基础设施的安全配置不能等出事再补应该作为上线检查清单的固定项。Nacos防护措施控制台必须开启认证修改默认账号密码使用强密码。通过防火墙限制Nacos控制台的访问来源生产环境只允许运维跳板机IP访问。配置文件的敏感信息加密存储Nacos本身提供了配置加密插件或者用Jasypt这一类库在配置值层面做加解密。服务注册信息设置访问白名单避免未知服务随意注册。Knife4j防护措施生产环境通过配置开关关闭文档聚合功能这是最直接的做法。如果必须在生产环境开放文档给联调用一定要加权限控制Knife4j支持通过过滤器做访问控制或者把文档路径隐藏在随机字符串路径下再配合同盟网络限制访问。更推荐的做法生产环境用独立的内网文档站点管理接口文档与运行环境隔离。# 生产环境关闭Knife4j knife4j: enable: false production: true这里要注意仅靠依赖配置可能还不够还要确保网关层不把这些接口暴露到公网。网关路由和Knife4j的excludePath要联动配置不能出现网关层放行了但Knife4j组件自己又开了个入口的情况。4. 内部服务间调用的安全比外部认证更容易被忽略的一环4.1 服务间调用为什么不能直接裸奔微服务架构里业务服务之间大量的接口互调是常态。订单服务要调用户服务获取用户信息支付服务要调订单服务更新订单状态。如果这些内部接口不做任何认证一旦攻击者探明了内网服务地址或者通过某个SSRF漏洞打进了内网就能直接调用内部接口产生比外部攻击更严重的影响。我在渗透测试时见过这样的系统内部管理接口没有任何鉴权只要知道了路径就能增删改查所有数据。有种观点认为内网是可信的服务间调用不需要认证。这个观点在云原生架构下已经站不住脚了。微服务的网络边界是动态的服务实例随时伸缩、迁移单纯依靠网络隔离来保证服务间安全已经被越来越多的安全实践否定。4.2 服务间身份认证的两种主流实践服务间认证最常见的是mTLS方案也就是双向TLS。网关和每个服务都配备证书通信时双方都要验证对方的证书建立双向信任。这个方案在Service Mesh里落地得比较成熟比如Istio的自动mTLS可以给网格内所有服务通信自动开启双向认证对业务代码零侵入。轻量级的方案是服务间调用统一携带Service Token由认证服务签发Token标识调用方应用身份目标服务验签后确认调用方合法。这个方案实现起来比mTLS简单但要做好Service Token的生命周期管理。我在项目中用过一个简单的模式内部调用统一通过Feign拦截器注入Header目标服务通过HandlerInterceptor统一校验。Configuration public class InternalApiAuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String path request.getRequestURI(); // 内部接口统一走/internal前缀 if (!path.startsWith(/internal)) { return true; } String serviceToken request.getHeader(X-Service-Token); if (!serviceTokenProvider.validate(serviceToken)) { response.setStatus(HttpStatus.FORBIDDEN.value()); return false; } return true; } }我的建议是服务规模不大、基础设施条件有限时可以先用Service Token方案做个基础防护等引入Service Mesh后无缝切换到mTLS。不管用哪种方案内部接口要统一走独立的前缀路径方便集中治理。4.3 SSRF漏洞与内网拓扑泄露的连带风险服务间调用还有一个容易被忽视的风险——SSRF。一些业务服务为了集成外部系统允许用户传入一个URL然后服务端去请求这个URL。如果服务端请求时没有限制目标地址用户可以把目标指向内网地址比如Nacos的8848端口、Redis的6379端口通过服务端作为跳板去探测和攻击内网。这类攻击的可怕之处在于利用了业务正常的请求通道。防护上要从几个维度下手校验URL协议只允许HTTP(S)解析域名后校验解析出的IP是否为内网IP禁止重定向或者重新校验重定向地址对出站请求统一走代理并配置目的地址白名单。细节上还要注意某些URL解析库对特殊格式的解析有差异比如http://baidu.com127.0.0.1这类变体可能绕过简单的校验需要充分测试。5. 常见问题与排查技巧实录5.1 认证失败排查速查表我在支持团队排查认证相关问题时总结了一张速查表遇到问题照着查能省不少时间。这里按问题现象、可能原因、排查手段、解决方案四个维度整理。问题现象可能原因排查手段解决方案用户登录后访问接口偶发401Token过期时间过短客户端未处理刷新查看客户端是否有统一刷新逻辑引入Refresh Token机制客户端拦截401后自动刷新部分服务校验Token失败其他服务正常各服务使用的验签公钥不一致或算法不同对比各服务配置的JWT密钥和算法统一由配置中心管理公钥避免环境间配置漂移Token有效但网关不认网关和服务使用两套认证逻辑网关读不到用户会话检查网关是否独立解析Token统一Token签发和解析逻辑做成公共组件接口文档在生产环境可访问配置未关闭Knife4j或者网关未限制文档路径检查knife4j.production配置和网关路由生产环境关闭文档或做IP白名单限制某个服务被刷大量请求网关限流只覆盖了入口服务到服务的调用没有限流查看监控系统服务间调用量对服务间调用引入租户级限流有非法Token通过了验签算法混淆攻击或密钥泄露检查JWT解析日志看alg字段统一算法白名单轮换泄露密钥5.2 我踩过一次的坑日志把关键敏感信息打了全量有一次客户反馈线上出了安全问题排查下来是网关的访问日志里把整个Authorization Header打了出来运维同事把日志同步到了第三方的日志分析平台平台端权限没控好出了泄露。这个教训之后我把全项目的日志规范重新过了一遍。现在会在日志配置里对Authorization、Cookie、Token、手机号、身份证号这类敏感字段做脱敏处理比如logback里自定义了一个脱敏Pattern凡是指定的关键字输出时只保留前几位和后几位。pattern %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){token([a-zA-Z0-9\-_.]), token***}%n /pattern这个手段不算多高级但确实能挡住一大批因为日志导致的信息泄露事故。做安全建设时很多团队喜欢买高精尖的防护设备但基础动作先做到位了效果往往更立竿见影。5.3 安全巡检的固定动作清单最后放一份我每次做微服务安全巡检时的固定动作清单你可以直接拿去用检查Nacos、Knife4j、Swagger等开发组件在生产环境是否可访问。扫描代码仓库检查是否有硬编码的密钥、密码、Token。核查网关层的认证过滤器是否对所有非白名单路径生效。抽查5-10个关键业务接口确认不仅做了角色校验还做了数据属主校验。查看日志配置确认敏感字段已脱敏。检查JWT解析逻辑确认算法白名单已开启。确认Refresh Token可以被主动吊销并能强制用户的Access Token失效。核实服务间调用是否带了身份凭证内部接口是否有独立鉴权。这些动作每季度做一次比等出了安全事故再到处救火要省心得多。从我个人这几年在微服务安全上实操的体会来看认证授权这块的漏洞往往不是技术多高深而是一些该做的基础工作被省略了有了网关鉴权就不再下沉校验、配置中心能开公网就懒得关、日志脱敏觉得麻烦就跳过。安全没有银弹把基础功课补齐一层一层把防线垒起来大部分攻击路径就自然被堵住了。希望这篇总结能帮你把自己的系统从头到尾过一遍把该焊的门焊死。
返回列表