当前位置: 首页 > news >正文

一次由Flowable定时器引发的“401”悬案:深入解析异步线程中的Token传递

目录

一、案情回顾:一个“善变”的接口

二、第一轮排查:代码中的“不在场证明”

三、真相浮现:同步与异步的“岔路口”

四、技术深潜:揭秘Flowable后台的“神秘工人”——Job Executor

五、解决办法

方案一:创建内部专用接口(M2M调用)

方案二:通过流程变量传递用户身份(推荐)

六、结语


在微服务与工作流引擎深度结合的今天,我们时常会遇到一些“灵异”现象:同样的代码逻辑,在流程的一条分支上畅通无阻,而在另一条分支上却神秘地失败。本文将复盘一个真实的业务场景,带您像侦探一样,从蛛丝马迹中追踪并破解一个由Flowable定时器在异步线程中引发的401 Unauthorized悬案。

一、案情回顾:一个“善变”的接口

我们的业务场景是一个常见的项目审核流程:审稿人可以对用户提交的项目执行“通过”或“驳回”操作。

  • 驳回路径:流程正常,项目状态被成功更新。
  • 通过路径:流程在定时器触发后,项目状态更新失败,下游的项目服务甚至未收到任何请求日志

【原始代码:UpdateStatusListener.java

@Slf4j
@Component("updateStatusListener")
public class UpdateStatusListener implements JavaDelegate {@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate ProjectClient projectClient;@Overridepublic void execute(DelegateExecution execution) {String processInstanceId = execution.getProcessInstanceId();boolean pass = (boolean) execution.getVariable("pass");String businessKey = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult().getBusinessKey();ProjectExamineDTO projectExamineDTO = new ProjectExamineDTO();projectExamineDTO.setProjectId(Collections.singletonList(businessKey));// 2:审核不通过,3:审核通过projectExamineDTO.setStatusNumber(pass ? 3 : 2);Result<Boolean> booleanResult = projectClient.examineProject(projectExamineDTO);log.info("项目审核远程调用接口结果:{}", booleanResult);}
}

可以看到,无论是通过(pass=true)还是驳回(pass=false),最终都调用了同一个远程方法 projectClient.examineProject,参数也仅仅是 statusNumber 不同。直觉告诉我们,这不应该有任何问题。

然而,日志输出了截然不同的结果,为我们提供了第一条关键线索:

驳回结果项目审核远程调用接口结果: Result(code=0, msg=success, data=true)

通过结果项目审核远程调用接口结果: Result(code=401, msg=未授权, data=null)

在“通过”路径中,接口明确返回了401 未授权。难道是远程调用时丢失了Token?

二、第一轮排查:代码中的“不在场证明”

根据401的线索,我们立刻将目光投向了负责传递Token的Feign拦截器。

由此可以看出,在远程调用时,拦截器已经将当前请求线程的请求头内容放到了该次请求头中,那为什么还会出现401呢?为什么驳回流程成功调用呢?分析到这里,已经明确请求头已正确携带,那是为什么呢?

这不由得让我们开始怀疑目标服务接口 examineProject 的代码是否存在逻辑漏洞。

【目标服务代码】

不由得开始怀疑起远程调用的接口是否存在逻辑错误,

@PutMapping(value = "examineProject")
@Operation(summary = "项目审核(statusNumber 0:草稿,1:提交, 2:不通过,3通过)")
@LogOperation("项目审核(statusNumber 0:草稿,1:提交, 2:不通过,3通过)")
@PreAuthorize("hasAnyAuthority('asap:poapproject:examineProject')")
public Result<Boolean> examineProject(@RequestBody ProjectExamineDTO projectExamineDTO) {System.out.println("开始审核项目,项目审核内容是"+projectExamineDTO);Boolean result = poapProjectService.examineProject(projectExamineDTO);return new Result<Boolean>().ok(result);
}@Override
@Transactional(rollbackFor = Exception.class)
public Boolean examineProject(ProjectExamineDTO projectExamineDTO) {logger.info("开始审核项目,项目审核内容是:{}", projectExamineDTO);// ... 省略了详细的数据库操作和消息通知逻辑 ...// 关键点:对于 statusNumber = 2 和 3 的处理,除了调用的通知方法不同,// 核心业务逻辑和权限校验完全一致。if(projectExamineDTO.getStatusNumber() == 2){noticeProducer.projectReviewFail(projectRequest);} else if(projectExamineDTO.getStatusNumber() == 3){noticeProducer.projectReviewSuccess(projectRequest);}return true;
}

在仔细阅读了这段看似平平无奇的业务代码后,我们发现,它并没有针对“通过”和“驳回”设置不同的认证策略。@PreAuthorize 注解的权限检查也是一视同仁。

代码本身似乎无懈可击,那么问题究竟出在哪里?线索,一定隐藏在我们尚未关注到的细节之中。

三、真相浮现:同步与异步的“岔路口”

让我们重新审视“通过”与“驳回”两条链路的本质区别,终于发现了那处微小但致命的差异。

  • 驳回链路: 审稿人驳回项目后,流程引擎根据 pass=false 的条件,立即、同步地驱动流程执行“项目驳回”服务任务,并调用监听器。
  • 通过链路: 审稿人通过项目后,流程根据 pass=true 的条件,驱动流程进入“等待归档”的用户任务,并激活了边界定时器。流程在此暂停,直到定时器在未来某个时间点触发后续的服务任务。

读到这里,您是否已洞悉了问题的关键?

路径一:审稿人驳回(同步执行)

审稿人通过API(携带用户Token)完成任务,激活了一个HTTP请求线程。从任务完成、网关判断、再到服务任务调用updateStatusListener,整个过程一气呵成,全部发生在这个同一个HTTP请求线程的生命周期内。因此,由Spring Security管理的用户安全上下文(Token)得以完整保留,Feign拦截器也能成功获取并传递它。

路径二:审稿人通过 -> 定时器触发(异步执行)

审稿人通过API完成任务,流程进入带定时器的等待节点,此时API请求已处理完毕并返回,HTTP请求线程随之销毁。一分钟后,Flowable的后台作业线程(Job Executor)发现了到期的定时器,并开始驱动流程继续执行。

关键的转折点:此时执行updateStatusListener的,是一个全新的、独立的后台线程。它与之前用户的任何HTTP请求都毫无关联,SecurityContextHolder是空的,Feign拦截器自然也就获取不到任何Token,导致了401错误的发生。

四、技术深潜:揭秘Flowable后台的“神秘工人”——Job Executor

为了让大家彻底理解定时器的工作原理,我们补充一下Flowable Job Executor的知识。

1. 核心理念

Job Executor是Flowable引擎中负责处理异步任务和定时事件的核心组件,它是一个持久化、可靠的调度器。当流程遇到需要“等待”或“稍后执行”的节点时,引擎会:

  1. 创建一个描述了“何时”需要“何事”的**作业(Job)**对象。
  2. 将这个Job对象持久化到数据库中。
  3. 结束当前线程,释放资源。

随后,Job Executor会像一个独立的闹钟服务,在后台不断轮询数据库,执行到期的作业。

2. 定时器的生命周期(数据库视角)

  • 创建作业 (Job Creation)
    • 当流程进入“等待归档”节点,引擎会计算出定时器的触发时间(DueDate = 当前时间 + 1分钟),并向作业表(如 ACT_RU_TIMER_JOB)中插入一条记录。这条记录包含了流程实例ID、执行ID、到期时间、作业类型等所有必要信息。即使服务器重启,这个“待办事项”也不会丢失。
  • 获取作业 (Job Acquisition)
    • Job Executor的获取线程会定期轮询作业表,执行类似 SELECT * FROM ACT_RU_TIMER_JOB WHERE DUE_DATE_ <= NOW() 的查询,找出所有到期的作业,并使用悲观锁锁定它们以防集群环境下的重复执行。
  • 执行作业 (Job Execution)
    • Job Executor的执行线程池中的某个线程会获取到这个作业。它解析作业信息,得知需要触发一个边界定时器事件,然后更新流程实例的状态,驱动流程前进到下一个节点(“执行审核通过逻辑”),并最终调用我们的updateStatusListener

这个机制保证了流程的健壮性和异步处理能力,但也天然地造成了执行上下文的隔离,从而引发了我们文章开头的那场401悬案。

五、解决办法

针对“后台线程丢失用户身份导致401”这个典型问题,业界有几种成熟且标准的解决方案。

它们的核心思想都是一致的: 要么为后台线程提供一种新的、受信任的认证方式,要么将原始用户的身份信息“接力”到后台线程中。

方案一:创建内部专用接口(M2M调用)

这种方案的核心是区分“外部用户调用”和“内部服务间调用”。

1. 在目标服务中创建一个新的Controller方法

这个新接口专门用于后台服务间的调用,它不依赖于用户Token,因此不添加 @PreAuthorize 权限校验

/*** 工作流后台线程调用,不需要携带 token* @param projectExamineDTO* @return*/
@PutMapping(value = "examineProjectPass")
@Operation(summary = "项目审核(statusNumber 3通过)")
@LogOperation("项目审核(statusNumber)")
public Result<Boolean> examineProjectPass(@RequestBody ProjectExamineDTO projectExamineDTO) {System.out.println("开始审核项目,项目审核内容是"+projectExamineDTO);Boolean result = poapProjectService.examineProject(projectExamineDTO);return new Result<Boolean>().ok(result);
}

2. 如何保障安全?

这个“免Token”的内部接口绝不意味着“不设防”。它的安全性由网络层面来保障:

  • API网关: 在API网关(Gateway)上配置路由规则,禁止外部流量访问所有以 /internal/ 开头的路径。只允许来自内部网络的、可信服务的流量通过。
  • 防火墙/VPC: 在云环境中,确保服务都部署在同一个虚拟私有云(VPC)内,服务间的调用不暴露在公网上。

3 修改UpdateStatusListener进行逻辑分支

在监听器中,根据是“通过”(异步)还是“驳回”(同步)来决定调用哪个Feign接口。

@Override
public void execute(DelegateExecution execution) {
String processInstanceId = execution.getProcessInstanceId();
boolean pass = (boolean) execution.getVariable("pass");
String businessKey = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult()
.getBusinessKey();
ProjectExamineDTO projectExamineDTO = new ProjectExamineDTO();
projectExamineDTO.setProjectId(Collections.singletonList(businessKey));
// 2:审核不通过,3:审核通过
projectExamineDTO.setStatusNumber(pass ? 3 : 2);
if(pass){// 该远程调用由工作流后台线程调用,应该选择调用系统内部服务,不携带token跳过网关校验Result<Boolean> booleanResult = projectClient.examineProjectPass(projectExamineDTO);log.info("项目审核通过远程调用接口结果:{}", booleanResult);
}else{// 该请求由 http 请求调用Result<Boolean> booleanResult = projectClient.examineProject(projectExamineDTO);log.info("项目审核驳回远程调用接口结果:{}", booleanResult);
}
}
  • 优点:实现简单、逻辑清晰,能快速解决问题。
  • 缺点:需要在目标服务新增接口,并依赖网关进行安全管控。

方案二:通过流程变量传递用户身份(推荐)

这种方案更规范,它保留了操作的“归属”,即后台线程执行的业务,我们依然知道是哪个用户最初触发的。

1. 在完成任务时,将用户ID存入流程变量

在审稿人完成审核任务(无论是通过还是驳回)的代码中,获取当前用户的唯一标识(如用户ID),并存入流程变量。

// 在调用 taskService.complete(...) 的地方
Map<String, Object> variables = new HashMap<>();
variables.put("pass", true);
// 从安全上下文中获取当前用户ID
Long currentUserId = SecurityUtils.getUserId(); 
// 将操作员ID存入流程变量
variables.put("operatorUserId", currentUserId);taskService.complete(taskId, variables);

2. 在UpdateStatusListener中取出用户ID,并添加到请求头

后台线程执行监听器时,可以从流程变量中取回这个ID。

@Override
public void execute(DelegateExecution execution) {// ...Long operatorUserId = (Long) execution.getVariable("operatorUserId");// ... 准备 projectExamineDTO ...// 调用一个特殊的、能动态添加Header的Feign方法// 注意:这里的实现需要对FeignClient做一些定制Result<Boolean> booleanResult = projectClient.examineProjectWithOperator(projectExamineDTO, operatorUserId);// ...
}

3. 定制FeignClient以动态添加Header

需要有一种机制,能将 operatorUserId 这个变量放入Feign请求的Header中,例如 X-Operator-User-Id: 123。这通常通过自定义RequestInterceptor来实现。

4. 修改目标服务的接口和安全逻辑

目标服务的 /examineProject 接口需要修改,使其能够识别这个特殊的Header。安全逻辑也会相应调整:

  • 如果请求头中包含 Authorization (用户Token),则正常走用户权限校验。
  • 如果请求头中包含 X-Operator-User-Id (内部调用),则跳过Token校验,但会信任这个Header,并将操作记在 operatorUserId 名下。当然,这个逻辑也需要API网关的配合,确保 X-Operator-User-Id 这个Header是可信的内部服务添加的,而不是外部伪造的。
  • 优点:安全性更高,审计链路清晰(始终知道操作的发起人),接口统一。
  • 缺点:实现相对复杂,需要定制Feign客户端和调整下游服务的安全策略。

六、结语

通过这次复盘,我们不仅解决了一个棘手的401问题,更深入理解了工作流引擎中同步与异步执行的本质区别。在设计复杂的、包含异步或定时逻辑的流程时,我们必须时刻关注执行上下文的传递问题,尤其是用户身份和安全凭证。无论是通过流程变量手动传递关键信息,还是设计专门的内部免鉴权接口,确保后台线程拥有正确的执行权限,都是保证系统稳定运行的关键。















 

http://www.dtcms.com/a/428833.html

相关文章:

  • 龙华哪有做网站设计手加工外包加工网
  • C语言循环与函数详解
  • 昆明做网站那家好建设网站商城
  • seo的网站建设建站基础:wordpress安装教程图解 - 天缘博客
  • centos 7.2 做网站婚礼请柬电子版免费制作app
  • 宜兴网站建设公司qq推广网站
  • 网站建设 设备推广app软件
  • 【动态规划:子数组/子串系列】单词拆分 环绕字符串中唯⼀的子字符串
  • 做网站服务器要什么系统推广怎么推广
  • qq网站登录北京网站优化推广分析
  • CNN的可视化:特征图与卷积核可视化方法(代码实现)
  • 读写RPLMN等APDU log显示为FF FF FF……问题研究
  • CKAD-CN 考试知识点分享(8) 升级与回滚
  • 网站建设公司该如何选择服务称赞的项目管理平台
  • 哪里做网站比较快wordpress主题 视频
  • 网站建设实训总结范文品牌市场营销策略
  • 网站界面设计软件网站备案去哪注销
  • 网页设计感十足的网站移动开发软件
  • LangChain 中 “附加 OpenAI 函数” 和 “附加 OpenAI 工具”
  • 山东住房和城乡建设厅网站登陆平面设计必学软件
  • 凡客建站官网登录入口Wordpress仿制网站
  • 网站开发技术的发展开发者门户网站是什么意思
  • GIS 相关基础知识
  • 陕西有色建设有限公司官方网站花生壳动态域名做网站
  • 企业网站seo平台不孕不育网站建设总结
  • 做短连接的网站织梦门户网站
  • 怎么做网站流量统计wordpress值得买模板
  • 【DRAM存储器五十八】LPDDR5介绍--IO结构,VREF和ODT有什么关系?
  • 增塑剂网站建设wordpress google
  • 什么是网站的主页seo优化排名工具