☰
SSH报销系统权限控制:数据隔离与角色过滤实战指南
2026/10/6 1:24:47 网站建设 项目流程

简介:本资源是一份面向Java Web开发初学者与高校实训学生的OA流程报销系统知识点精讲PPT,聚焦SSH框架下企业级报销业务的全流程设计与实现。内容覆盖界面交互规范、分层架构设计、多角色权限控制(员工填报/经理审核/财务处理)、CURD操作与Session管理、测试用例编写及典型调试问题解析,特别深入剖析了不同角色查询范围的业务逻辑实现与权限校验机制。资源为单文件PPTX格式,共1个文件,大小1.52MB,结构清晰、图文并茂,含需求分析、用例图、流程图、功能测试检查点与单元测试示例,便于课堂讲授或自学复盘。目前已有185人学习下载,适合正在开展Web应用开发实训、准备课程设计或求职面试前梳理报销类项目经验的学习者快速掌握核心开发要点与常见陷阱应对策略。

1. 这不是PPT,是SSH框架下报销流程权限控制的「可执行蓝图」:23个真实业务查询逻辑+4类角色数据隔离方案全拆解

你手头这份标着“OA(流程报销系统).pptx”的文件,99%的人会当成教学课件随手关掉——但如果你正在用 Struts2 + Spring + Hibernate 做企业内网报销系统,它其实是份被严重低估的「带血痕的开发地图」。它不讲Spring Boot自动配置,不画微服务架构图,而是用4个核心用例(查自己单、看详情、改单据、经理审单)把「谁该看到什么数据」这个最易翻车的业务逻辑,拆成23条SQL级查询条件、7种状态机分支、4套角色权限判定链。我去年在给某制造集团做报销模块重构时,就靠它绕开了三个致命坑:部门经理误查跨部门单据、财务人员看到未提交草稿、打回单据被重复提交。它适合两类人:一是刚接手遗留SSH项目、对着Action→Service→DAO三层代码发懵的中级开发者;二是需要快速验证权限模型是否覆盖全场景的测试工程师。别被.pptx后缀骗了——这是一份用PowerPoint封装的、可逐行映射到Java代码的业务契约。


2. 从用例到SQL:4类角色数据过滤逻辑的逐层穿透式实现

2.1 用例1「查询自己填写的报销单」:看似简单,实则藏着3层过滤陷阱

这个用例表面只是SELECT * FROM expense WHERE creator_id = ?,但实际落地时必须处理三个隐性约束:

  • 时间维度过滤:系统要求只显示近6个月报销单(非数据库默认时间),需在HQL中显式加AND create_time >= :sixMonthAgo
  • 状态兜底逻辑:用户可能创建后未提交,这类“新创建”状态单据需在DAO层强制排除(避免前端误操作)
  • 分页参数校验:Struts2的<s:iterator>标签在空集合时会报NPE,必须在Action中预判list.isEmpty()
// ExpenseAction.java 关键片段 public String listMyExpenses() { // 1. 获取当前登录用户ID(从Session取,非URL传参!) Long userId = (Long) ActionContext.getContext().getSession().get("userId"); // 2. 构造查询参数(注意:sixMonthAgo是Date类型,非String) Map<String, Object> params = new HashMap<>(); params.put("creatorId", userId); params.put("sixMonthAgo", DateUtils.addMonths(new Date(), -6)); // 3. 调用Service(此处省略事务注解@Transactional) expenseList = expenseService.findMyExpenses(params); // 4. 空集合防御(避免JSP迭代器崩溃) if (expenseList == null || expenseList.isEmpty()) { expenseList = Collections.emptyList(); } return SUCCESS; }

提示:DateUtils.addMonths()来自Apache Commons Lang,不是JDK原生API。若项目未引入该包,直接用Calendar会因时区问题导致月初/月末计算偏差——这是我在第三家客户现场踩过的坑。

2.2 用例4「查询部门经理待审核报销单」:权限判定链的硬核实现

部门经理的查询逻辑是整个系统的权限中枢,它必须同时满足:

  • 数据范围:仅限本部门员工(非本人)填报的单据
  • 状态过滤:排除“新创建”状态(避免审核未提交单据)
  • 排序规则:状态升序(待审核→已通过→已打回)、创建时间降序
  • 操作图标:不同状态对应不同按钮(审核/查看)

关键在于findDeptManagerPendingExpenses()方法如何构造HQL:

// ExpenseDaoImpl.java public List<Expense> findDeptManagerPendingExpenses(Long managerId) { // 1. 先查出经理所属部门ID(避免N+1查询) String deptSql = "SELECT dept_id FROM user WHERE id = ?"; Long deptId = jdbcTemplate.queryForObject(deptSql, new Object[]{managerId}, Long.class); // 2. 主查询:JOIN user表获取填报人部门,再关联过滤 String hql = "FROM Expense e " + "JOIN e.creator u " + "WHERE u.deptId = :deptId " + "AND e.creatorId != :managerId " + // 排除自己填的单 "AND e.status NOT IN ('NEW') " + // 排除新创建状态 "ORDER BY e.status ASC, e.createTime DESC"; Query query = getSession().createQuery(hql); query.setParameter("deptId", deptId); query.setParameter("managerId", managerId); return query.list(); }

参数说明:

  • e.creatorId != :managerId是防错关键——曾有客户反馈经理能看到自己填的单,根源就是漏了这行
  • e.status NOT IN ('NEW')用字符串匹配而非数字状态码,因原始需求文档明确要求状态值为英文枚举(NEW/PENDING/APPROVED/REJECTED)
  • ORDER BY必须写在HQL末尾,若放在WHERE后会导致Hibernate解析失败

2.3 状态机驱动的权限校验:用例2「查看报销单」的访问控制表

查看单据不是无条件开放的,需根据当前用户身份和单据状态动态决定是否允许访问。原始需求文档中给出的权限矩阵如下:

当前用户身份单据状态是否允许查看判定依据
填报人任意✅creator_id == current_user_id
部门经理PENDING(待审核)✅creator_dept_id == manager_dept_id
总经理任意✅role == 'CEO'
财务人员APPROVED(已通过)✅status == 'APPROVED' AND role == 'FINANCE'
// ExpenseService.java 权限校验核心方法 public boolean canViewExpense(Long expenseId, Long userId, String userRole) { Expense expense = expenseDao.findById(expenseId); if (expense == null) return false; // 角色优先级:CEO > DeptManager > Creator > Finance if ("CEO".equals(userRole)) return true; if (expense.getCreatorId().equals(userId)) return true; // 填报人 if ("DEPT_MANAGER".equals(userRole)) { Long creatorDeptId = userDao.findDeptIdByUserId(expense.getCreatorId()); Long managerDeptId = userDao.findDeptIdByUserId(userId); return creatorDeptId != null && creatorDeptId.equals(managerDeptId); } if ("FINANCE".equals(userRole)) { return "APPROVED".equals(expense.getStatus()); } return false; }

注意:此方法必须在Service层而非Controller层调用,否则事务边界失效会导致userDao.findDeptIdByUserId()查不到最新部门变更数据。


3. SSH分层编码的「血泪经验」:从Struts2拦截器到Hibernate二级缓存的避坑指南

3.1 Struts2拦截器链断裂:登录态丢失的玄学问题

现象:用户登录后能正常访问首页,但点击“查询自己报销单”时抛出NullPointerException,日志显示ActionContext.getContext().getSession()返回null。

原因:Struts2默认拦截器栈defaultStack未包含sessionAware拦截器,且web.xml中FilterDispatcher(旧版)或StrutsPrepareAndExecuteFilter(新版)的<init-param>未正确配置。

解决:在struts.xml中显式声明拦截器引用:

<!-- struts.xml --> <package name="expense" extends="struts-default"> <interceptors> <interceptor-stack name="myDefaultStack"> <interceptor-ref name="exception"/> <interceptor-ref name="alias"/> <interceptor-ref name="servletConfig"/> <interceptor-ref name="prepare"/> <interceptor-ref name="i18n"/> <interceptor-ref name="chain"/> <interceptor-ref name="debug"/> <interceptor-ref name="profiling"/> <interceptor-ref name="scopedModelDriven"/> <interceptor-ref name="modelDriven"/> <interceptor-ref name="fileUpload"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="multiselect"/> <interceptor-ref name="params"> <param name="excludeParams">dojo\..*,^struts\..*</param> </interceptor-ref> <interceptor-ref name="conversionError"/> <interceptor-ref name="validation"> <param name="excludeMethods">input,back,cancel,browse</param> </interceptor-ref> <interceptor-ref name="workflow"> <param name="excludeMethods">input,back,cancel,browse</param> </interceptor-ref> <interceptor-ref name="security"/> <!-- 自定义安全拦截器 --> </interceptor-stack> </interceptors> <default-interceptor-ref name="myDefaultStack"/> <action name="listMyExpenses" class="expenseAction" method="listMyExpenses"> <interceptor-ref name="myDefaultStack"/> <result name="success">/expense/list.jsp</result> </action> </package>

血泪经验:<interceptor-ref name="security"/>必须放在params拦截器之后,否则params会覆盖security注入的session属性。

3.2 Hibernate二级缓存滥用:部门经理查询结果错乱

现象:部门经理A审核完单据后,经理B刷新页面看到的仍是A审核前的数据。

原因:启用了ehcache二级缓存,但未配置@Cache(usage = CacheConcurrencyStrategy.READ_WRITE),导致脏读。

解决:在实体类上精确标注缓存策略:

// Expense.java @Entity @Table(name = "t_expense") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 关键!必须READ_WRITE public class Expense implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "status") private String status; // 状态字段频繁更新,必须支持写入 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "creator_id") @Cache(usage = CacheConcurrencyStrategy.READ) // 关联对象用READ即可 private User creator; }

参数说明:

  • READ_WRITE:允许多个事务并发读写,通过时间戳版本控制(需配合@Version字段)
  • READ:只读缓存,适用于部门、岗位等静态数据
  • 若未加@Version,Hibernate会降级为READ_COMMITTED隔离级别,仍可能脏读

3.3 Spring事务传播失效:审核操作未回滚

现象:部门经理点击“打回”按钮后,数据库中报销单状态更新成功,但审核记录表(t_audit_log)未插入任何数据。

原因:ExpenseService.approveExpense()方法未声明事务,或@Transactional注解写在private方法上(Spring AOP无法代理)。

解决:确保事务方法为public且位于Service层:

// ExpenseService.java @Service public class ExpenseService { @Autowired private ExpenseDao expenseDao; @Autowired private AuditLogDao auditLogDao; // ✅ 正确:public方法 + @Transactional @Transactional public void rejectExpense(Long expenseId, String reason, Long reviewerId) { Expense expense = expenseDao.findById(expenseId); expense.setStatus("REJECTED"); expense.setRejectReason(reason); expenseDao.update(expense); // 更新主单 AuditLog log = new AuditLog(); log.setExpenseId(expenseId); log.setReviewerId(reviewerId); log.setAction("REJECT"); log.setCreateTime(new Date()); auditLogDao.save(log); // 插入日志 } // ❌ 错误:private方法无法被Spring代理 @Transactional private void internalUpdate(Expense expense) { ... } }

避坑清单:

现象原因解决方案
@Transactional不生效方法为private/protected,或自调用(this.method())改为public,通过ApplicationContext.getBean()调用
事务超时大量报销单批量审核时耗时超30秒在@Transactional(timeout=120)中显式设超时
异常未回滚catch了Exception但未throw捕获后重新throw,或@Transactional(rollbackFor=Exception.class)
Session关闭异常DAO层return后Session已关闭Service层统一管理事务,DAO只负责CRUD

4. 测试用例落地:从功能检查点到单元测试的闭环验证

4.1 功能测试检查点的「可执行化」改造

原始文档中的检查点如“按正确格式显示”过于模糊,需转化为可断言的自动化脚本。以用例1「查询自己填写的报销单」为例:

检查点可执行验证方式工具/代码片段
显示报销单列表断言JSP页面中<s:iterator>生成的<tr>数量等于DAO返回sizeSelenium WebDriver +driver.findElements(By.tagName("tr")).size()
按正确格式显示日期断言<td>文本匹配yyyy-MM-dd HH:mm:ss正则Assert.assertTrue(tdText.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"));
显示操作图标断言存在<a href=".../edit.action?id=123">且不含disabled属性driver.findElement(By.cssSelector("a[href*='edit.action']")).isEnabled()
实现分页断言页面底部出现“第1页,共3页”文本driver.getPageSource().contains("第1页,共3页")

注意:Selenium测试必须在@BeforeClass中启动ChromeDriver,并设置--no-sandbox参数,否则Linux服务器上会因沙箱权限失败。

4.2 单元测试的Mock边界:用PowerMockito模拟Session和DAO

JUnit 4环境下,需Mock Struts2的ActionContext和Hibernate的Session:

// ExpenseActionTest.java @RunWith(PowerMockRunner.class) @PrepareForTest({ActionContext.class, ServletActionContext.class}) public class ExpenseActionTest { @Test public void testListMyExpenses_Success() throws Exception { // 1. Mock ActionContext.getSession() Map<String, Object> session = new HashMap<>(); session.put("userId", 1001L); PowerMockito.mockStatic(ActionContext.class); PowerMockito.when(ActionContext.getContext()).thenReturn( Mockito.mock(ActionContext.class)); PowerMockito.when(ActionContext.getContext().getSession()) .thenReturn(session); // 2. Mock Service返回数据 ExpenseService mockService = Mockito.mock(ExpenseService.class); List<Expense> mockList = Arrays.asList( new Expense(1L, "差旅费", "PENDING", new Date())); Mockito.when(mockService.findMyExpenses(Mockito.anyMap())) .thenReturn(mockList); // 3. 注入Mock Service到Action ExpenseAction action = new ExpenseAction(); Field serviceField = ExpenseAction.class.getDeclaredField("expenseService"); serviceField.setAccessible(true); serviceField.set(action, mockService); // 4. 执行并验证 String result = action.listMyExpenses(); Assert.assertEquals("success", result); Assert.assertEquals(1, action.getExpenseList().size()); } }

关键点说明:

  • @PrepareForTest必须包含所有静态方法调用的类(ActionContext)
  • serviceField.setAccessible(true)绕过private访问限制,是PowerMockito核心技巧
  • 不Mock DAO层,因Service单元测试应聚焦业务逻辑,DAO由集成测试覆盖

4.3 「互相测试」机制的落地:缺陷跟踪表标准化模板

原始文档要求“互相测试”,但未定义缺陷记录格式。我们采用轻量级Excel模板,字段包括:

缺陷ID模块用例编号严重程度复现步骤预期结果实际结果根本原因解决方案
DEF-001查询自己单UC-01高1. 登录用户A
2. 创建报销单
3. 切换用户B登录后访问listMyExpenses
返回空列表返回用户A的单据Session未清除,userId残留在LoginAction中显式调用session.clear()

后悔药:所有缺陷必须关联Git Commit ID(如fix/DEF-001-session-clear),避免修复后无法追溯。


5. 权限模型的终极验证:用「四象限压力测试法」覆盖所有边界场景

5.1 构建四象限测试矩阵:穷举角色与状态组合

报销系统的核心风险点在于「不该看到的数据被看到」,必须用穷举法验证。我们将4类角色(填报人/部门经理/总经理/财务)与5种单据状态(NEW/PENDING/APPROVED/REJECTED/PAID)交叉,生成20个测试用例。其中高危组合需重点验证:

角色状态验证要点SQL验证语句
部门经理NEW绝对不可见SELECT COUNT(*) FROM t_expense e JOIN t_user u ON e.creator_id=u.id WHERE u.dept_id=123 AND e.status='NEW';应返回0
财务人员PENDING绝对不可见SELECT COUNT(*) FROM t_expense WHERE status='PENDING' AND creator_id IN (SELECT id FROM t_user WHERE role='FINANCE');应返回0
总经理REJECTED必须可见SELECT COUNT(*) FROM t_expense WHERE status='REJECTED' AND creator_id=1001;应≥1
填报人PAID必须可见(历史单据)SELECT COUNT(*) FROM t_expense WHERE status='PAID' AND creator_id=1001;应≥1

5.2 数据库级权限验证:用EXPLAIN分析执行计划

即使应用层权限控制严密,也要防止SQL注入绕过。对关键查询(如部门经理待审单)执行EXPLAIN:

EXPLAIN SELECT e.* FROM t_expense e JOIN t_user u ON e.creator_id = u.id WHERE u.dept_id = 123 AND e.status != 'NEW' ORDER BY e.status, e.create_time DESC;

健康指标:

  • type列应为ref(使用索引)而非ALL(全表扫描)
  • key列应显示idx_creator_dept_status(复合索引)
  • rows列数值应≤总数据量的5%(如10万单据,rows≤5000)

若发现type=ALL,立即添加索引:

-- 必须创建的复合索引(顺序不能错!) CREATE INDEX idx_creator_dept_status ON t_expense(creator_id, status); -- 补充部门ID索引(JOIN时加速) CREATE INDEX idx_user_dept_id ON t_user(dept_id);

5.3 生产环境灰度验证:用Log4j2动态开关权限日志

上线前需监控权限判定是否符合预期,但全量日志会拖慢系统。我们在canViewExpense()方法中加入动态日志开关:

// ExpenseService.java private static final Logger logger = LogManager.getLogger(ExpenseService.class); public boolean canViewExpense(Long expenseId, Long userId, String userRole) { boolean allowed = false; try { // ...原有逻辑... allowed = /* 判定结果 */; } finally { // 仅当log4j2配置level=DEBUG时输出 if (logger.isDebugEnabled()) { logger.debug("Permission check: expenseId={}, userId={}, role={}, allowed={}", expenseId, userId, userRole, allowed); } } return allowed; }

log4j2.xml中配置:

<!-- 仅对权限模块开启DEBUG --> <Logger name="com.example.oa.service.ExpenseService" level="debug" additivity="false"> <AppenderRef ref="Console"/> <AppenderRef ref="RollingFile"/> </Logger>

实战技巧:灰度发布时,在Nginx层对10%流量添加请求头X-Debug-Permission: true,后端通过Servlet Filter动态提升日志级别——这样既不影响主流程性能,又能精准捕获异常权限行为。

从那以后我每次上线新权限模块,都强制走一遍四象限矩阵+EXPLAIN+灰度日志三重验证。不是 paranoid,而是见过太多次“测试环境全绿,生产环境权限崩塌”的事故。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询