site logo

Marico's space

我用 Spring Boot 和 Spring Security 构建 3 层审批引擎

服务器技术 2026-04-25 22:55:52 2

译者按:最近看到一篇关于 Spring Boot 审批引擎的文章,写得非常扎实。作者不仅讲了 CRUD,还深入到了状态机、服务层授权、审计追踪这些生产级系统必备的东西。我试着整理了一下,分享给大家——尤其是服务层授权守卫这部分,非常有启发。

为什么我要做这个

大多数后端教程教的都是 CRUD。注册、登录、获取列表。挺好的——但这无法让你为真实企业实际运行的系统做好准备。

几乎每个组织都有某种形式的审批工作流:员工提交请假申请,经理审核,部门负责人最终批准。当这些流程存在于邮件和电子表格中时,事情会丢失,审计变得不可能,权限滥用也无法被发现。

我想构建一个能真正解决这个问题的系统——有真正的状态机逻辑、强制执行的角色边界、完整的审计追踪,以及你在生产后端中会看到的那种安全架构。这就是我构建的东西,下面是它的工作原理。

在线 Swagger UI: https://workflow-approval-system-zq0a.onrender.com/swagger-ui.html
GitHub: https://github.com/DeepsanBhandari/workflow-approval-system

核心问题:状态机比 CRUD 更难

审批工作流的根本挑战不是端点——而是状态完整性。一个工作流请求有多个状态:

DRAFT → PENDING_MANAGER → PENDING_ADMIN → APPROVED
                      ↘               ↘
                    REJECTED        REJECTED
                      ↘
               CHANGES_REQUESTED

每个转换都有规则:

  • 只有 EMPLOYEE 可以提交 DRAFT
  • 只有 MANAGER 可以操作 PENDING_MANAGER
  • 只有 ADMIN 可以操作 PENDING_ADMIN
  • 不能跳级
  • 不能操作已完成的工作流

如果在服务层不强制执行这些,无论你的控制器级检查有多好,你的 API 都会有漏洞。

架构:分层 + DTO 优先

系统遵循严格的分层架构:

Controller Layer   →   接收 HTTP 请求,立即委托
Service Layer      →   拥有所有业务逻辑和授权守卫
Repository Layer   →   纯数据访问,无业务逻辑

这不仅仅是整洁代码的理念——它有具体的安全好处。如果把授权逻辑放在控制器中,在测试或内部集成中通过直接的服务调用很容易绕过。服务层的授权始终会被强制执行,无论服务如何被调用。

每个 API 契约都通过 DTO 强类型化——WorkflowResponseApprovalStepResponseApiResponseVoid。没有实体泄露到外部世界。这通过设计强制实现了角色间的零数据泄露。

安全:无状态 JWT 与自定义过滤器链

我使用自定义 Spring Security 过滤器实现了无状态 JWT 认证。这里有一个重要的架构决策:

Token 验证发生在边界。业务逻辑与认证 concerns 完全解耦。

自定义 JwtAuthenticationFilter 继承 OncePerRequestFilter。它:

  1. 提取 Authorization: Bearer <token> 请求头
  2. 使用密钥验证 token 签名
  3. 加载 UserDetails 并设置 SecurityContextHolder
  4. 传递给过滤器链中的下一个过滤器
// Filter 内部的概念流程
String token = extractToken(request);
if (token != null && jwtService.isTokenValid(token)) {
    UserDetails userDetails = userDetailsService.loadUserByUsername(
        jwtService.extractUsername(token)
    );
    UsernamePasswordAuthenticationToken authToken =
        new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities()
        );
    SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);

为什么是无状态的?因为无状态 JWT 意味着服务器上不需要会话存储。系统可以水平扩展,无需 sticky sessions。每个请求都是自包含的——token 携带身份声明。

密码使用 BCryptPasswordEncoder 哈希存储。从不以明文存储,从不记录。

服务层授权守卫(关键洞察)

这是整个系统最重要的架构决策。

这里是用控制器级角色检查的问题:

// ❌ 脆弱——仅在 HTTP 边界强制执行
@PreAuthorize("hasRole('MANAGER')")
@PostMapping("/{id}/approve")
public ResponseEntity approve(@PathVariable Long id) {
    workflowService.approve(id); // 内部没有守卫
}

如果 workflowService.approve() 从调度器、测试或另一个服务内部调用,角色检查就会被绕过。

这是我使用的替代方法:

// ✅ 无论调用者是谁都被强制执行
public WorkflowResponse approve(Long workflowId, String approverUsername) {
    Workflow workflow = workflowRepository.findById(workflowId)
        .orElseThrow(() -> new WorkflowNotFoundException(workflowId));

    User approver = userRepository.findByUsername(approverUsername)
        .orElseThrow(() -> new UserNotFoundException(approverUsername));

    // 守卫:这个审批者是否被允许操作这个工作流状态?
    validateApproverCanAct(workflow.getStatus(), approver.getRole());

    // 守卫:工作流是否处于可操作状态?
    validateWorkflowIsActionable(workflow.getStatus());

    // ... 继续状态转换
}

validateApproverCanAct 在 Manager 试图操作 PENDING_ADMIN 或反之时抛出 UnauthorizedActionException。这不是 Spring Security 的问题——它是业务规则授权,它存在于它应该存在的位置:服务层。

状态转换引擎

审批步骤是顺序的,不是并行的。每个转换都是明确的:

private void validateAndTransition(Workflow workflow, ApprovalAction action, User actor) {
    WorkflowStatus current = workflow.getStatus();

    switch (current) {
        case PENDING_MANAGER:
            requireRole(actor, Role.MANAGER);
            if (action == ApprovalAction.APPROVE) {
                workflow.setStatus(WorkflowStatus.PENDING_ADMIN);
            } else if (action == ApprovalAction.REJECT) {
                workflow.setStatus(WorkflowStatus.REJECTED);
            } else {
                workflow.setStatus(WorkflowStatus.CHANGES_REQUESTED);
            }
            break;
        case PENDING_ADMIN:
            requireRole(actor, Role.ADMIN);
            if (action == ApprovalAction.APPROVE) {
                workflow.setStatus(WorkflowStatus.APPROVED);
            } else {
                workflow.setStatus(WorkflowStatus.REJECTED);
            }
            break;
        default:
            throw new InvalidWorkflowStateException(current);
    }
}

没有状态可以跳过。没有参与者可以在其层级外操作。无效的转换抛出类型化异常,被全局异常处理器捕获。

审计追踪:每个操作都被记录

每个审批、拒绝或变更请求都被记录为 ApprovalStep 实体:

ApprovalStep {
    workflowId,
    actorUsername,
    actorRole,
    action (APPROVE / REJECT / REQUEST_CHANGES),
    comment,
    timestamp
}

GET /api/workflows/{id}/history 返回完整的顺序历史。对于任何真实的审批系统这是必须的——你需要知道谁做了什么以及何时做的。

测试:JUnit 5 + Mockito 达到 80%+ 覆盖率

我把测试覆盖率重点放在最重要的地方——业务规则所在的服务层。

示例:测试 Manager 不能审批 ADMIN 级别的步骤:

@Test
void managerCannotApproveAdminLevelStep() {
    Workflow workflow = createWorkflowWithStatus(WorkflowStatus.PENDING_ADMIN);
    User manager = createUserWithRole(Role.MANAGER);

    assertThrows(UnauthorizedActionException.class, () ->
        workflowService.approve(workflow.getId(), manager.getUsername())
    );
}

测试 happy path 是容易的。测试边缘情况——Manager 试图插队时会发生什么、有人试图操作已批准的工作流时会怎样——这才是真正覆盖率来源。

CI/CD 流水线:零手动步骤

GitHub Actions 流水线在每次推送到 master 时运行:

jobs:
  build-and-deploy:
    steps:
      - name: Run Tests
        run: mvn test          # 快速失败——在合并前捕获回归

      - name: Build Docker Image
        run: docker build -t workflow-approval-system .

      - name: Push to Registry
        run: docker push ...

      - name: Deploy to Render
        # Render 在镜像推送时自动从 registry 部署

如果测试失败,流水线停止。没有什么坏东西能到达生产环境。Dockerfile 使用多阶段构建——在一个阶段编译,在精简的 JRE 镜像中运行——保持最终镜像精简。

接下来我会添加什么

对真正的企业系统缺什么保持诚实:

  1. 通知——当工作流到达你的层级时触发邮件/webhook
  2. Kafka 用于审计事件——将审批事件发布到消息代理而不是同步数据库写入
  3. 历史端点分页——现在没问题,但在大型审计日志下会出问题
  4. Refresh token 轮换——当前 JWT 实现使用单一 token;生产环境需要 refresh + revocation

关键要点

如果你正在构建自己的审批系统或类似的状态机后端:

  • 把授权守卫放在服务层,不仅仅是控制器
  • 显式建模状态转换——不要依赖散落在代码库各处的 if/else 链
  • 每个修改状态的操作都应该被审计
  • 业务逻辑上 80%+ 的测试覆盖率是可实现的,值得投入——不是为了指标,而是因为测试迫使你思考边缘情况

完整系统已上线,通过 Swagger 完整文档化,可在 GitHub 获取。如果你对任何实现细节有疑问,留言——很乐意深入讨论任何部分。


Deepsan Bhandari 是一名 Java/Spring Boot 后端工程师,位于尼泊尔,目前在 IOE Purwanchal Campus 完成土木工程学士学位,同时构建生产级后端系统。开放接受远程实习和自由职业后端工作。

GitHub: github.com/DeepsanBhandari | LinkedIn: linkedin.com/in/deepsan-bhandari