ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSH报销系统权限控制:数据隔离与角色过滤实战指南

SSH报销系统权限控制:数据隔离与角色过滤实战指南 简介本资源是一份面向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. 从用例到SQL4类角色数据过滤逻辑的逐层穿透式实现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 MapString, 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 ListExpense findDeptManagerPendingExpenses(Long managerId) { // 1. 先查出经理所属部门ID避免N1查询 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/REJECTEDORDER 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 nameexpense extendsstruts-default interceptors interceptor-stack namemyDefaultStack interceptor-ref nameexception/ interceptor-ref namealias/ interceptor-ref nameservletConfig/ interceptor-ref nameprepare/ interceptor-ref namei18n/ interceptor-ref namechain/ interceptor-ref namedebug/ interceptor-ref nameprofiling/ interceptor-ref namescopedModelDriven/ interceptor-ref namemodelDriven/ interceptor-ref namefileUpload/ interceptor-ref namecheckbox/ interceptor-ref namemultiselect/ interceptor-ref nameparams param nameexcludeParamsdojo\..*,^struts\..*/param /interceptor-ref interceptor-ref nameconversionError/ interceptor-ref namevalidation param nameexcludeMethodsinput,back,cancel,browse/param /interceptor-ref interceptor-ref nameworkflow param nameexcludeMethodsinput,back,cancel,browse/param /interceptor-ref interceptor-ref namesecurity/ !-- 自定义安全拦截器 -- /interceptor-stack /interceptors default-interceptor-ref namemyDefaultStack/ action namelistMyExpenses classexpenseAction methodlistMyExpenses interceptor-ref namemyDefaultStack/ result namesuccess/expense/list.jsp/result /action /package血泪经验interceptor-ref namesecurity/必须放在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只读缓存适用于部门、岗位等静态数据若未加VersionHibernate会降级为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(timeout120)中显式设超时异常未回滚catch了Exception但未throw捕获后重新throw或Transactional(rollbackForException.class)Session关闭异常DAO层return后Session已关闭Service层统一管理事务DAO只负责CRUD4. 测试用例落地从功能检查点到单元测试的闭环验证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?id123且不含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和DAOJUnit 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() MapString, 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); ListExpense 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必须包含所有静态方法调用的类ActionContextserviceField.setAccessible(true)绕过private访问限制是PowerMockito核心技巧不Mock DAO层因Service单元测试应聚焦业务逻辑DAO由集成测试覆盖4.3 「互相测试」机制的落地缺陷跟踪表标准化模板原始文档要求“互相测试”但未定义缺陷记录格式。我们采用轻量级Excel模板字段包括缺陷ID模块用例编号严重程度复现步骤预期结果实际结果根本原因解决方案DEF-001查询自己单UC-01高1. 登录用户A2. 创建报销单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_idu.id WHERE u.dept_id123 AND e.statusNEW;应返回0财务人员PENDING绝对不可见SELECT COUNT(*) FROM t_expense WHERE statusPENDING AND creator_id IN (SELECT id FROM t_user WHERE roleFINANCE);应返回0总经理REJECTED必须可见SELECT COUNT(*) FROM t_expense WHERE statusREJECTED AND creator_id1001;应≥1填报人PAID必须可见历史单据SELECT COUNT(*) FROM t_expense WHERE statusPAID AND creator_id1001;应≥15.2 数据库级权限验证用EXPLAIN分析执行计划即使应用层权限控制严密也要防止SQL注入绕过。对关键查询如部门经理待审单执行EXPLAINEXPLAIN 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若发现typeALL立即添加索引-- 必须创建的复合索引顺序不能错 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配置levelDEBUG时输出 if (logger.isDebugEnabled()) { logger.debug(Permission check: expenseId{}, userId{}, role{}, allowed{}, expenseId, userId, userRole, allowed); } } return allowed; }log4j2.xml中配置!-- 仅对权限模块开启DEBUG -- Logger namecom.example.oa.service.ExpenseService leveldebug additivityfalse AppenderRef refConsole/ AppenderRef refRollingFile/ /Logger实战技巧灰度发布时在Nginx层对10%流量添加请求头X-Debug-Permission: true后端通过Servlet Filter动态提升日志级别——这样既不影响主流程性能又能精准捕获异常权限行为。从那以后我每次上线新权限模块都强制走一遍四象限矩阵EXPLAIN灰度日志三重验证。不是 paranoid而是见过太多次“测试环境全绿生产环境权限崩塌”的事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表