ARTICLE DETAIL

资讯详情

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

若依二次开发避坑指南:权限标识、数据权限与前端缓存问题排查

若依二次开发避坑指南:权限标识、数据权限与前端缓存问题排查 手上有若依框架二次开发任务的朋友今天这篇内容应该能帮到你。这是我做 contract-security-ruoyi合同安全管理系统第十六天的开发记录或者说是一篇完整的 Bug 排查复盘。今天一整天我基本都耗在排查几个“诡异”问题上一会儿接口莫名其妙 401一会儿用户看到了不该看的数据还有前端页面死活不刷新。这些不是框架本身的硬伤但如果你没把若依的底层逻辑吃透二次开发时踩进去一定会懵一阵子。这篇文章我会把这些坑的完整排查过程、根因分析和修复方案都记录下来顺带把我在若依框架里摸出来的调试技巧和避坑清单也一并整理出来供你参考。不管你是刚开始用若依还是已经被二开折磨得头大这篇文章应该都能对得上你的需求。我会把具体的报错现象、排查路径、修复代码都写清楚适合边看边对照自己的项目。1. 项目背景与二次开发思路拆解1.1 从框架选型开始为什么最终选了若依先交代一下项目背景。contract-security-ruoyi 是一个合同安全管理系统核心需求包括合同全生命周期管理、用印登记、供应商信息维护、合同履行风险预警等。因为它涉及到合同数据和用印流程权限安全这一块的需求特别重不同部门只能看本部门的合同角色之间要有严格的菜单和数据隔离操作日志必须留痕可审计。这类系统最合适的做法是在一个成熟的后台管理框架上做二次开发而不是从零手撸一套权限体系。我们当时在技术选型时也纠结过后面主要对比了两条路线一条是若依RuoYi另一条是 JeecgBoot。简单说下结论。JeecgBoot 的代码生成器确实功能多自带 Online 在线开发模式能省掉一部分重复劳动。但它的模块拆分比较重对项目结构的侵入性更强学习成本也不低。如果团队里多数人对 Spring Boot 熟悉但对 Jeecg 生态不熟上手反而慢。若依这边则更“传统”也更“透明”底层是 Spring Boot MyBatis Vue 这种大家都很熟的组合代码生成、权限注解、数据权限过滤这些能力都是轻量级实现改起来容易看明白出了问题也好定位。对我们这类“合同权限审计”业务比较明确的项目若依这种简洁直观的架构反而更好扩展。当然选若依你就得接收它的“框架脾气”很多机制它是不会明说的你得在实际开发和 Bug 排查过程中自己摸清楚。今天记录的这几个问题基本都属于这一类。1.2 contract-security-ruoyi 的模块结构与核心功能项目到目前为止已经完成了大概三分之二的功能。基于若依框架的基础能力我们做了这样一些二次开发合同管理模块包含合同录入、审批流转、电子归档、到期预警。这一块直接用了若依的代码生成器生成了基础 CRUD再手动加了很多业务字段和审批流状态机。用印管理模块核心是控制谁能发起用印、用印文件必须挂合同编号、用印过程要留痕。这里涉及比较细的角色权限配置。安全审计模块我们自己加了登录日志风险识别、异常操作告警、敏感字段脱敏。对应改造了若依的登录接口和 SysLogininfor 表结构。系统管理模块沿用若依自带的用户、角色、菜单、部门管理在角色那边增加了数据权限维度确保合同信息只在授权范围内可见。这些功能在设计阶段看起来都还算清晰但真正跑起来之后问题就藏在细节里了。下面两个 Bug 是我今天最头疼的一个涉及接口权限校验一个涉及数据权限过滤恰好是若依二次开发里最容易出问题的两个点。2. 第一个Bug接口无缘无故返回401越权问题初现2.1 现象描述接口时好时坏登录状态看起来正常但请求被拦上午一到公司测试就丢了一个问题过来合同列表接口GET /contract/security/list在系统里访问一会儿正常一会儿就返回 401 未授权。更诡异的是这个接口是通过若依前端框架自带的方法发起的请求而且登录状态没有过期刷新页面之后立刻再点查询有时候就又好了。我一开始以为是测试那边网络问题或者 Token 过期但后来自己在本地环境复现了两三次确定不是偶发网络问题。请求头里的Authorization明明带上了 Token服务端这边却还是给了 401。这个问题如果不解决后面所有接口都可能出现这种“正常访问被拦截”的情况影响面很大。2.2 排查过程从请求头到 Redis再到权限标识遇到这种时候我习惯从三个方向同时排查前端请求头是否正常、后端安全过滤器是否抛了异常、数据库里的权限配置是否有问题。第一步先在浏览器 DevTools 的 Network 面板里看具体请求确认请求头长什么样。正常情况下若依框架会携带这样的请求头信息Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... Content-Type: application/json如果这个请求头缺失或者 Token 值不对那 401 就正常。但实测下来前端这边是正常带了 Token 的而且这个 Token 能用 Postman 调通其他接口。那问题就不在 Token 是否过期上而是“部分请求被过滤链拦截了”。第二步去看后端的异常日志。在若依框架里401 异常一般会由认证过滤器JwtAuthenticationTokenFilter抛出我直接打开了日志过滤条件找到了抛 401 的那条记录。日志里报的错误是AuthenticationException: Full authentication is required to access this resource看到这个异常第一反应是再核对一遍接口的权限配置。在这个项目中我们用PreAuthorize注解配置接口权限比如PreAuthorize(ss.hasPermi(contract:security:list)) GetMapping(/list) public TableDataInfo list(SysContract contract) { return getDataTable(contractService.selectContractList(contract)); }这个注解的意思是当前用户必须拥有contract:security:list这个权限标识才能访问这个接口。那问题就来了——用户明明已经登录且角色配置了相关权限为什么权限校验还是不通过第三步去数据库里查一下菜单权限标识到底怎么配的。若依的权限体系里菜单表sys_menu的perms字段就是权限标识符的来源。角色关联菜单用户关联角色最终通过登录时缓存的permissions集合来判断用户有哪些权限。查完sys_menu这张表后问题一下就清楚了菜单表里确实有一条contract:security:list的权限记录但是我发现代码里面实际用的是contract:security:list而数据库里有一条记录是contract:security:query。另外列表接口我配的是list后缀而新增、修改、删除接口分别配的是add、edit、remove。问题是菜单表里那一条记录是系统初始化的演示数据perms字段写的是contract:security:query和我的接口注解完全对不上。2.3 根因定位与修复权限标识符不一致所以问题的根因很简单接口注解上写的权限标识符和数据库sys_menu表里perms字段的值不一致导致权限校验时找不到对应的权限框架直接返回 401。这属于典型的二次开发“改代码不配菜单”或者“复制代码后没同步改权限标识”导致的失误。修复方法也很直接把sys_menu表里的perms字段改成和接口注解一致UPDATE sys_menu SET perms contract:security:list WHERE menu_id 1234;然后清理缓存。若依框架会把用户的权限信息缓存在 Redis 里权限变更之后需要刷新缓存或者重新登录才会生效。我直接用的若依后台自带的“刷新缓存”工具这个路径是系统管理 - 系统工具 - 缓存监控 - 清空全部缓存实际可以用redis-cli FLUSHDB快速处理不过为了不影响其他在线用户我建议只清当前用户的缓存或者让测试重新登录一遍。修复之后同样一个接口再访问就不会出现 401 了。提示每次用代码生成器生成新模块后最好第一时间去菜单管理页面把权限标识符改好命名为“模块首字母:实体名:操作类型”并保持代码注解和数据库字段完全一致。这是若依二开最容易忽略的地方没有之一。3. 第二个Bug数据权限配置逻辑反向用户看到了不该看的合同数据3.1 问题描述同一部门的人只能看到本部门数据却看到了全公司的合同下午刚消停一会儿测试又来报了一个问题这次更严重属于数据越权部门 A 的用户登录后在合同列表里能看到部门 B 的合同记录。这在一个合同安全管理系统里是不能接受的。按照需求说明系统上线后应该启用“数据权限”功能——不同部门的用户只能查看本部门的合同记录只有当角色的数据权限范围配置为“全部数据”时才能看到所有部门的合同。我这边在代码里是加了若依的数据权限注解DataScope的但加完之后用户还是能看到全量数据没有触发过滤效果。3.2 排查过程从 SQL 日志里看框架到底执行了什么排查这种问题我不会一上来就猜代码逻辑而是先把实际操作时真正执行的 SQL 打印出来看。若依框架里日志默认会打印 mapper 执行语句你可以在application.yml里把日志级别调成debug来确认logging: level: com.ruoyi: debug然后重新请求列表接口去控制台找 mybatis 执行的 SQL 语句。正常情况下加入数据权限过滤的 SQL 应该会自动拼接一段AND条件大概长这样SELECT ... FROM sys_contract WHERE contract_id ? AND (contract.dept_id 1001)但实际执行的时候我发现 SQL 里面完全没有拼接数据权限条件。问题很明显要么是注解没生效要么是 SQL 语句里表别名不匹配导致框架压根没识别出应该给哪张表加过滤条件。仔细看我们的 mapper XML 文件列表查询主表用的别名是cselect idselectContractList parameterTypeSysContract resultMapSysContractResult SELECT c.contract_id, c.dept_id, c.supplier_name FROM sys_contract c where if testcontractName ! null and contractName ! AND c.contract_name LIKE CONCAT(%, #{contractName}, %) /if /where /select而我在 Controller 层写的DataScope注解是这样的PreAuthorize(ss.hasPermi(contract:security:list)) DataScope(deptAlias d, userAlias u) GetMapping(/list) public TableDataInfo list(SysContract contract) { startPage(); ListSysContract list contractService.selectContractList(contract); return getDataTable(list); }这里犯了一个很经典的错误若依的DataScope注解是通过表别名alias来拼接过滤条件的默认它会在 SQL 末尾拼接AND d.dept_id IN (...)所以你要么给主表取个别名d注意不是c要么在注解里指定正确的别名。而我这里实际用的是c框架找不到d这个别名就直接放弃了拼 SQL数据权限自然没有生效。3.3 修复方案别名对齐并确认参数传递找到原因后修复方案有两个方向一是改 XML 查询语句把主表别名改成d同时把查询条件里的前缀都改成d.select idselectContractList parameterTypeSysContract resultMapSysContractResult SELECT d.contract_id, d.dept_id, d.supplier_name FROM sys_contract d where if testcontractName ! null and contractName ! AND d.contract_name LIKE CONCAT(%, #{contractName}, %) /if /where /select二是直接修改注解参数明确告诉框架deptAlias cDataScope(deptAlias c, userAlias c)我这边选择的是第二种因为改动最小不影响其他查询逻辑。但是要注意如果查询语句里关联了多张表你需要在DataScope里同时指定部门和用户对应的别名框架会分别对部门字段和用户字段做条件拼接。改完后重新执行接口SQL 里成功出现了预期的过滤条件AND (c.dept_id IN (1001, 1002))数据权限恢复了正常。注意若依的数据权限过滤其实依赖一个隐藏参数传递机制。当你在 Controller 里加DataScope时切面会把SysUser对象里关于数据权限的配置角色数据范围、部门信息放入BaseEntity的params字段然后由 Mapper XML 中的dataScope参数自动拼入 SQL。如果你在 Service 层自己手动调用了 mapper 方法而没有经过 Controller 层的请求上下文数据权限参数可能就传递不过去SQL 也不会拼接过滤条件。我后来排查这个问题时也遇到过这种情况解决办法是在 Service 里手动调setParams()传入当前用户的数据权限参数。4. 前端联调与页面数据不刷新的排查实录4.1 前端怎么打断点调试 Bug给不熟悉浏览器调试的朋友后端问题解决得差不多了前端这边又冒出来一个现象级问题新增了一条合同数据后列表页却不刷新显示的还是旧数据。看起来像是“缓存”问题但排查起来没那么简单这里先介绍一下我排查前端问题时的一套固定套路。如果你对前端调试还不太熟先把 F12 开发者工具用起来。在 Vue 项目中最常用的调试方式有两个第一种是在 Sources 面板打 debugger 断点。打开浏览器 F12找到 Sources源代码面板按Ctrl P可以快速搜索文件找到对应的.vue文件或.js文件在代码行号上点击一下就能加一个红点断点。之后重新触发这个前端函数浏览器就会停在断点处这时候你可以在右侧的 Scope 面板里一步一步查看变量值、调用栈非常直观。第二种是配合 Vue Devtools 插件。这个插件可以在浏览器侧栏直接查看当前页面组件的数据状态、路由信息、store里的数据变化借助它你能快速判断页面上显示的数据到底是来自接口返回还是来自前端缓存、路由缓存等。我先用 Vue Devtools 看了列表页的组件数据发现 created 钩子根本没有重新执行tableData还停留在上一次请求返回的数据上。这说明页面组件可能被路由缓存了组件根本没被重新创建。4.2 页面数据不刷新路由缓存和 keepAlive 的坑问题定位得很快。在若依框架的路由配置中AppMain.vue的keep-alive中缓存了部分页面组件。凡是需要缓存的页面路由配置里会设置keepAlive: true。被缓存的页面组件在路由切换后不会销毁下次再次进入时也不会重新走created或mounted生命周期钩子而是直接走activated激活钩子。我这边新增合同后调用了重新查询列表的getList()方法但它只写在created()里created() { this.getList(); }该组件开启了 keepAlive所以新增合同后跳转回来created 并不会触发页面自然还停留在旧数据上。修复方案是新增activated生命周期钩子在里面也加上重新查询的逻辑activated() { this.getList(); }这么改完后从新增页面返回列表页面时组件被重新激活就会重新请求列表数据并刷新表格。4.3 前端其他容易误判的“Bug”路由跳转后数据残留其实这种缓存问题还有另一个容易误判的场景新增或编辑时引用了同一个页面组件但组件内的form表单数据没有重置导致第二次编辑时表单里残留了上一条数据。出现这类情况你需要在表单打开的代码里加一个显式的重置逻辑比如reset() { this.form { contractId: undefined, contractName: undefined, supplierName: undefined }; }然后在打开新增接口那里调用一次。另外如果发现点击菜单跳转后页面刷新了但操作按钮依然可点也可能是组件缓存导致isDisabled状态没被更新用activated钩子重新初始化页面状态是一个标准解法。前端的问题调试起来不像后端那样有明确的日志和堆栈多数时候要靠断点盯数据和生命周期钩子的执行顺序。建议你在项目里统一把 keepAlive 逻辑理清楚哪些页面确实需要缓存哪些不需要避免在项目后期被这种“页面不刷新”的问题反复折磨。5. 若依框架二次开发避坑指南5.1 代码生成器的正确用法生成只是开始不是终点今天时间基本耗在两个权限问题上了但真要整体回顾一下若依二开的经验代码生成器是绕不开的一环。我们的合同管理模块、用印登记模块基本都是靠若依的代码生成器先跑出来的基础 CRUD。但是生成之后你千万别觉得完事了代码生成器生成的三层结构Controller、Service、Mapper只是最基础的标准版和实际业务需求之间还有一大截要手动改。用代码生成器时需要注意几点生成之前数据库表的设计要尽量规范。主键用id或xxx_id字段要有注释需要逻辑删除的话务必有del_flag字段需要乐观锁就加version字段。我在生成合同表的时候就吃过亏字段注释写得太随意生成到前端页面后表单标签全是一堆无意义的英文字段名后面手动改了一遍。生成之后Controller 层的权限注解要重新配置。默认生成的权限标识符是模块名:实体名:list比如contract:contract:list而这个模块名:实体名是生成器自动命名的和你在菜单管理里配置的菜单权限可能完全对不上这就会导致今天第 2 小节里 401 的问题。所以生成之后第一件事就是统一权限标识符。Service 层默认只有基本 CRUD如果你的业务有审批流、事务串联、多表关联更新等逻辑需要手动往里加不要试图只靠生成器解决所有业务问题。前端生成的 Vue 文件基本能用但查询表单和表格列的宽度、自定义按钮、状态标签比如把状态数字显示成中文标签都要重写。我习惯把生成的.vue文件当作“脚手架模板”正式业务页面最终都会改一半以上。代码生成器不是银弹它帮你把最繁琐的“搬运型 CRUD”做完了但真正的业务逻辑、权限控制、数据校验必须要二次开发人员自己补上去。5.2 事务、定时任务和数据库自动备份开发过程中另一个容易踩坑的地方是事务。若依框架基于 Spring Boot事务通常用Transactional注解来实现看起来很简单但有几个使用坑。第一个坑是事务只对 Spring 管理的 Bean 方法之间的调用有效。你在同一个类内部 A 方法调用 B 方法B 方法里加了Transactional但事务不会生效因为 Spring 事务是通过代理对象实现的类内部方法的自调用绕过了代理机制。如果你确实需要事务可以把内部调用拆到另一个 Service 里或者通过TransactionTemplate手动执行事务代码块。第二个坑是Transactional默认只能拦截 RuntimeException运行时异常。如果你在事务方法里 catch 了一个异常并打印日志常见的catch (Exception e)事务就不会回滚了。这个在合同审批流程里特别致命——如果审批业务中途抛了 exception但被 try-catch 吞掉了数据库就停留在中间状态后面排查数据对不上就得花老半天。另外一个跟项目稳定性相关的点是数据库备份。合同安全管理系统上线后数据库备份是底线要求。我们的方案是在服务器上用 crontab 定时执行一个 Shell 脚本来实现自动备份#!/bin/bash # 每天凌晨 2 点备份 ruoyi 数据库 DATE$(date %Y%m%d%H%M%S) mysqldump -uroot -pYourPassword ruoyi /data/backup/ruoyi_$DATE.sql find /data/backup -mtime 7 -name *.sql -exec rm -f {} \;脚本里除了备份还加了一个 7 天过期清理逻辑避免备份文件越积越多。你可以直接把这个脚本放到 crontab 里0 2 * * * /opt/scripts/backup_db.sh /var/log/backup_db.log 21需要注意mysqldump 备份的是逻辑数据如果在备份过程中有大量写入操作可能产生数据不一致的情况。如果要保证一致性建议用--single-transaction参数InnoDB 表来开启一个一致性快照能最大程度减少锁表影响。提示在量化或财务类系统里定时任务如果涉及到把钱、库存这类关键数字反复计算最好加上分布式锁或数据库乐观锁避免任务重跑导致数据翻倍。若依自带的定时任务模块Quartz也有这种隐患因为它的执行是基于数据库调度表的分布式部署时要特别注意重复执行问题。5.3 无法复现的 Bug 怎么处理一套方法论做二次开发久了你会发现最耗时的并不是那些一眼就能看出问题的 Bug而是那种“偶尔出现、重新登录就好了、怎么都复现不了”的玄学 Bug。今天最开始遇到的那个 401 问题也有点这个意思——不每次出现很容易让人以为环境问题。以我的经验处理这类问题时按下面三个步骤来会高效很多第一步加日志、加日志、加日志。不要急着去猜而是把可能出现问题的关键节点全部打上日志打印出请求参数、Token、用户 ID、权限标识符、过滤器名称。这是把“无法复现”变成“可复现”的最有效方法。第二步保留现场。如果用户反馈在某个特定环境比如特定浏览器、特定账号、特定日期才出现先不要让人家急着刷新页面而是在出问题时立刻导出浏览器 Network 记录、Redis 缓存快照、后端 Debug 日志。很多临时性 Bug 其实就是 Redis 缓存过期或某条配置被改的问题现场数据往往能一锤定音。第三步环境对比。把开发环境、测试环境、生产环境的代码版本、数据库脚本、Redis 版本、JDK 版本做一次全面对比。说实话我遇到过不少“测试环境正常生产环境报错”的 Bug最后都发现是环境变量或配置文件不一致导致的。比如错把一个配置项写死在了 application-dev.yml 里生产环境加载的值和本地不一样行为自然不同。5.4 若依和 Jeecg 的二次开发选择题顺带聊聊既然题目热词里有人问到若依和 Jeecg 对比我在选型阶段也花了一点时间顺便在这里多说两句。如果你的业务重点在“后台管理系统 权限 简单工作流”若依是更轻量、更透明的选择它的权限模型、代码风格、社区资料都很好找遇到问题基本都能搜索到现成答案。如果你需要建立大量报表、表单在线设计、拖拉拽生成页面这类偏“低代码”的需求Jeecg 会更顺手但代价是你得接受它更重的框架约束和更高的学习成本。对我们这个 contract-security-ruoyi 项目来说合同管理、用印、审计这些核心业务都需要深度定制我们宁可框架少给一点、自己写多一点也不能被框架绑死手脚。事实证明这个选择本身没问题但前提是你真的愿意花时间去读若依的源码和设计逻辑。今天坑我们的这两个问题说白了还是对若依权限模型的内部规则不够熟。最后再分享几个实操中的小技巧今天排查的 401 问题、数据权限越权问题、前端 keepAlive 不刷新问题其实每一类都对应一个固定的排查套路。这些套路我踩了几次坑才总结出来顺手也给你排查 401先去查请求头 Authorization 在不在径流量是对的但真正定位要往权限配置表sys_menu的perms字段去看和代码注解逐一比对别只看前端 Token。排查数据权限没生效别急着改代码先打印出实际执行 SQL看末尾有没有拼接数据权限条件。没有拼接就 80% 是表别名不对或数据权限参数传递问题。如果 SQL 拼接了但数据还是越权再去检查角色配置里数据范围是不是设的“全部数据”那个是最高权限所有越权的最前置条件。排查前端数据不刷新先看组件里有没有activated钩子再去看路由配置 keepAlive 是不是开了最后才去考虑是不是接口没返回新数据。我在这个项目里最后把getList()同时挂进了created和activated基本就把这个问题根上了。每个人的项目场景不同但若依框架的核心机制是相通的。你要是也在搞若依二次开发记住我把权限标识符、数据权限注解、路由缓存这三个点吃透至少能少踩一半的坑。今天这篇记录了十六天里的两个典型问题和一套调试方法论以后如果再碰到类似的坑欢迎在评论区把你的报错现象发出来我们可以一起看看怎么快速定位。
返回列表