
译者按:最近看到一篇关于 Spring Boot 审批引擎的文章,写得非常扎实。作者不仅讲了 CRUD,还深入到了状态机、服务层授权、审计追踪这些生产级系统必备的东西。我试着整理了一下,分享给大家——尤其是服务层授权守卫这部分,非常有启发。
大多数后端教程教的都是 CRUD。注册、登录、获取列表。挺好的——但这无法让你为真实企业实际运行的系统做好准备。
几乎每个组织都有某种形式的审批工作流:员工提交请假申请,经理审核,部门负责人最终批准。当这些流程存在于邮件和电子表格中时,事情会丢失,审计变得不可能,权限滥用也无法被发现。
我想构建一个能真正解决这个问题的系统——有真正的状态机逻辑、强制执行的角色边界、完整的审计追踪,以及你在生产后端中会看到的那种安全架构。这就是我构建的东西,下面是它的工作原理。
在线 Swagger UI: https://workflow-approval-system-zq0a.onrender.com/swagger-ui.html
GitHub: https://github.com/DeepsanBhandari/workflow-approval-system
审批工作流的根本挑战不是端点——而是状态完整性。一个工作流请求有多个状态:
DRAFT → PENDING_MANAGER → PENDING_ADMIN → APPROVED
↘ ↘
REJECTED REJECTED
↘
CHANGES_REQUESTED
每个转换都有规则:
EMPLOYEE 可以提交 DRAFTMANAGER 可以操作 PENDING_MANAGERADMIN 可以操作 PENDING_ADMIN如果在服务层不强制执行这些,无论你的控制器级检查有多好,你的 API 都会有漏洞。
系统遵循严格的分层架构:
Controller Layer → 接收 HTTP 请求,立即委托
Service Layer → 拥有所有业务逻辑和授权守卫
Repository Layer → 纯数据访问,无业务逻辑
这不仅仅是整洁代码的理念——它有具体的安全好处。如果把授权逻辑放在控制器中,在测试或内部集成中通过直接的服务调用很容易绕过。服务层的授权始终会被强制执行,无论服务如何被调用。
每个 API 契约都通过 DTO 强类型化——WorkflowResponse、ApprovalStepResponse、ApiResponseVoid。没有实体泄露到外部世界。这通过设计强制实现了角色间的零数据泄露。
我使用自定义 Spring Security 过滤器实现了无状态 JWT 认证。这里有一个重要的架构决策:
Token 验证发生在边界。业务逻辑与认证 concerns 完全解耦。
自定义 JwtAuthenticationFilter 继承 OncePerRequestFilter。它:
Authorization: Bearer <token> 请求头UserDetails 并设置 SecurityContextHolder// 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 返回完整的顺序历史。对于任何真实的审批系统这是必须的——你需要知道谁做了什么以及何时做的。
我把测试覆盖率重点放在最重要的地方——业务规则所在的服务层。
示例:测试 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 试图插队时会发生什么、有人试图操作已批准的工作流时会怎样——这才是真正覆盖率来源。
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 镜像中运行——保持最终镜像精简。
对真正的企业系统缺什么保持诚实:
如果你正在构建自己的审批系统或类似的状态机后端:
完整系统已上线,通过 Swagger 完整文档化,可在 GitHub 获取。如果你对任何实现细节有疑问,留言——很乐意深入讨论任何部分。
Deepsan Bhandari 是一名 Java/Spring Boot 后端工程师,位于尼泊尔,目前在 IOE Purwanchal Campus 完成土木工程学士学位,同时构建生产级后端系统。开放接受远程实习和自由职业后端工作。
GitHub: github.com/DeepsanBhandari | LinkedIn: linkedin.com/in/deepsan-bhandari