ARTICLE DETAIL

资讯详情

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

Java政务平台设计:RBAC权限与自研审批流实战

Java政务平台设计:RBAC权限与自研审批流实战 简介这是一份基于Java核心技术的政务综合平台设计源码主要面向政务信息化开发人员、Java全栈学习者以及需要快速搭建电子政务系统的技术团队可用于解决政府办事流程在线化、服务门户统一化等实际问题。压缩包总共包含一千五百八十七个文件大小约二十三兆字节其中既有六百七十九个JavaScript脚本、一百零三个HTML文档、九十五个CSS样式表、三十六个Java源文件也包含属性配置、PNG图片、GIF动画、地图文件等多种资源前端交互与后端业务逻辑组织得比较清晰。目前已有三百三十七人学习下载具有较高的工程参考价值。源码涵盖用户界面展示、请求响应处理、业务逻辑实现、数据交互以及项目配置管理等环节附带的Maven配置文件、Eclipse工程文件能够帮助理解自动化构建和依赖管理目录结构完整便于按模块检索适合用作文档学习、架构分析或二次开发基础。1. 基于Java核心技术的政务综合平台值得照着做一遍的项目样板如果你搜过Java工程师的岗位要求政务类JD里出现频率最高的技术词基本是Spring Boot、MyBatis、Redis、工作流和RBAC——这正是基于Java核心技术的政务综合平台设计源码的骨架。政务平台不做算法炫技它的难点在于把组织架构、人员权限、审批流转、留痕审计这些枯燥但绕不开的业务做严谨。这篇把方案拆成四段先定架构选型再把权限模型落地然后用最小代码把审批流转串起来最后列内网部署和国产化环境里最容易翻车的点。适合三类人准备政务类项目岗位的Java工程师、拿这个方向做课程设计与毕设的学生、需要用标准模板快速接小项目的个人开发者。不附带现成压缩包但设计思路可以直接照抄进工程。2. 先搭骨架Java政务平台选单体还是微服务多模块怎么分2.1 单体优先政务内网的量级撑不起微服务的运维成本政务综合平台的典型使用场景是内网环境注册用户几千到几万同时在线通常只有几百。这个量级下把所有Spring Cloud组件都上齐不是技术需要是给运维找麻烦。我见过一个区级事项平台拆了12个微服务最后部署脚本写了一百多行上线当天Nacos内存不够启动失败业务没跑起来先花两天调基础组件。真实政务项目里单体或模块化单体依然是大多数。模块化单体的思路是按业务边界拆成Maven多模块system管用户权限workflow管审批流转business管具体业务表common管公共工具。模块之间用接口隔离将来真要拆微服务从这些边界切就是现成的拆分点。这个选择的核心收益是事务好控制——一次审批要同时更新流程状态和审批记录在单体里一个Transactional就搞定拆成两个服务就要上分布式事务政务小项目根本没这个必要。还要分清一个概念政务综合平台不等于政务云。平台层才需要容器编排和组件集群业务平台只有一个jar包也能跑。如果评审专家问为什么不拆微服务答预留了模块边界当前并发量单体足够承载就行而不是一上来就把运维复杂度推给内网的同事。2.2 Maven多模块结构一次搭好不返工的目录与依赖我一般会按照下面的目录结构新建工程岗位上的新项目也沿用这套gov-platform ├── gov-common # 通用工具、统一返回、异常处理、注解 ├── gov-system # 用户、角色、菜单、部门、数据字典 ├── gov-workflow # 流程实例、审批记录、待办催办 ├── gov-business # 具体业务表事项、材料、台账 ├── gov-web # Web入口、Controller、定时任务、跨域配置 └── gov-web/src/main/resources ├── mapper # MyBatis XML按模块分包存放 └── application.ymlparent的pom里统一管理依赖版本子模块只声明自己用到的依赖这是避免依赖地狱的基本操作。Spring Boot这边我建议选一个发布超过一年的稳定版本新特性在政务项目里用不上稳定压倒一切。ORM用MyBatis-PlusJWT用jjwt工具集用Hutool就够。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency /dependencies版本组合说明上面这三组版本互相兼容Spring Boot 2.7.x对应MyBatis-Plus 3.5.x没有问题。如果你坚持用Spring Boot 3.x注意javax要整体换成jakartaMyBatis-Plus也要用适配新版本号的坐标我在这上面翻过车——旧代码里的javax.servlet包名全部编译失败半天时间都花在改包名上。政务项目求稳2.7这条线还能打好几年。2.3 实体类与建表SQL一次对齐代码生成器的三个必配参数政务项目的表结构有两个来源大一点的项目是DBA先建表后端用代码生成器自动生成实体、Mapper、Service课程设计和快速交付的场景反过来后端先定义实体类再根据实体生成建表SQL。这个需求的热度一直不低MyBatis-Plus根据实体类生成建表SQL在网上被反复问其实工程上更稳的顺序是先写建表脚本再让代码生成器反向生成实体。因为政务表都带sys_、gov_这类前缀表字段多到三四十个手写实体类错一个类型就够排查半天。FastAutoGenerator.create( jdbc:mysql://localhost:3306/gov_platform?useUnicodetruecharacterEncodingutf8, root, your_password) .globalConfig(builder - builder.author(dev).outputDir(src/main/java).enableSwagger()) .packageConfig(builder - builder.parent(com.gov) .entity(entity).mapper(mapper).service(service).controller(controller)) .strategyConfig(builder - builder.addInclude(sys_user, sys_role, sys_dept) .addTablePrefix(sys_, gov_) .entityBuilder().enableLombok().logicDeleteColumn(deleted) .mapperBuilder().enableBaseResultMap(true)) .execute();参数含义addInclude指定要生成哪几张表不要整库生成否则把第三方组件表也生成一遍工程里全是垃圾代码addTablePrefix在生成时会自动去掉前缀sys_user最终生成User实体而不是SysUserenableLombok让实体带上Data注解logicDeleteColumn声明逻辑删除字段deleted。生成完之后第一件事不是写业务而是把分页插件配好Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }提示政务列表页三件套是分页、筛选、导出分页插件不配列表接口的Page参数直接失效返回全表数据。这个配置虽然就三行但位置很关键漏了它会以最隐蔽的方式影响所有查询。代码生成器的输出里还要做两个小改造实体类加自动填充注解让create_time和update_time由MyBatis-Plus写入而不是业务代码里到处set当前时间Controller层统一继承一个BaseController返回Result包装类。这样第一个接口的写法就固定下来后面几十个业务接口都长一个样接手人看代码会舒服很多。3. 权限是命脉RBAC五张表与行级数据权限的Java落地3.1 RBAC五张表的DDL设计留好字段避免返工政务平台最忌讳权限写死在业务代码里。今天这个部门能看明天那个部门不能看需求方一个电话过来就要改配置配置化是唯一的出路。标准RBAC五张表就是用户、角色、菜单、用户角色关联、角色菜单关联我做设计时还会加上部门和数据字典两张基础表。核心DDL如下CREATE TABLE sys_user ( user_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(64) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt密文, dept_id BIGINT NOT NULL COMMENT 所属部门ID冗余字段, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, KEY idx_dept (dept_id) ) COMMENT用户表; CREATE TABLE sys_role ( role_id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(64) NOT NULL UNIQUE COMMENT 角色编码如DEPT_USER, role_name VARCHAR(64) NOT NULL COMMENT 角色名称, data_scope TINYINT NOT NULL DEFAULT 3 COMMENT 1全部 2本部门及下级 3本部门 ) COMMENT角色表; CREATE TABLE sys_menu ( menu_id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父菜单0为根, menu_name VARCHAR(64) NOT NULL, perms VARCHAR(128) COMMENT 权限标识如system:user:add, menu_type TINYINT COMMENT 1目录 2菜单 3按钮 ) COMMENT菜单表; CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) COMMENT用户角色关联表; CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL, PRIMARY KEY (role_id, menu_id) ) COMMENT角色菜单关联表;设计说明里有三个容易被忽略的点。第一sys_user里的dept_id是冗余字段本来可以通过部门表关联查出但行级数据权限要频繁按部门过滤冗余在主表里能省一次联查政务表上万的用户量联查也能拖慢接口。第二data_scope字段直接放在角色表而不是写死在代码里这样区县和市局的权限差异就能用一条配置区分开。第三菜单表里按钮级别也要入库事项审批的提交退回按钮要根据角色显隐很多政务权限问题就出在菜单隐藏了但按钮还在。3.2 Spring Security加JWT无状态认证的过滤链配置政务系统现在很少用纯Session方案后端经常是多实例部署Session要做分布式共享所以最常见的组合是登录接口签发JWT后续请求通过过滤器解析实现无状态认证。Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserDetailsService userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (StringUtils.hasText(header) header.startsWith(Bearer )) { String token header.substring(7); String username jwtTokenUtil.getUsername(token); if (StringUtils.hasText(username) SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }逻辑说明过滤器从请求头拿到token解析出用户名再调UserDetailsService加载用户和权限列表构建认证对象放入SecurityContextHolder后续方法级校验PreAuthorize才能拿到当前用户权限。token放在请求头而不是URL参数里是因为URL参数会被网关日志、浏览器历史记录下来政务项目的安全测评会盯这个点。配套的SecurityConfig几项必配参数放行白名单要包含登录接口、验证码接口、Swagger文档sessionCreationPolicy设置成STATELESS避免Spring Boot默认的Session策略干扰JWT模式JWT的过期时间一般给30分钟到2小时不要学互联网App给7天政务安全测评对token生命周期有硬性要求。验证码我用redis存一份key是UUIDvalue是答案登录时校验后立刻删除防止验证码复用这是接口防爆破的基本姿势。3.3 行级数据权限用注解加拦截器在SQL层收口普通RBAC管的是能不能进这个菜单行级数据权限管的是进了菜单后能看到哪些数据。市局账号看全市数据区县账号只看本区县普通办事员只看本部门——这就是行级权限的核心诉求。我的方案是注解加MyBatis拦截器Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DataScope { String deptAlias() default d; String deptColumn() default dept_id; }拦截器在执行SQL前拦截prepare方法解析方法上的DataScope注解向原SQL追加一段dept_id in可见部门集合的条件。可见部门集合根据当前用户角色上的data_scope字段算全部数据给空集合本部门及下级要递归查部门树只查本部门就直接用用户表冗余的dept_id。Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 从SecurityContextHolder取出当前登录用户和角色 // 2. 通过HandlerMethod解析目标Mapper方法是否有DataScope注解 // 3. 拿到BoundSql原SQL解析出from/where段追加dept_id in (…) // 4. 用追加后的SQL重建BoundSql放行执行 return invocation.proceed(); } }逻辑说明行级权限必须在SQL层限制不能在Service层查全表再过滤否则导出接口、报表接口很容易绕过Service过滤写出全量数据这是政务数据安全事故的高发点。DataScope注解加到查询方法上同一个Mapper方法在管理员和普通用户下的执行结果自动不同权限逻辑收敛在一处业务代码里不用到处写dept_id条件。提示拼接条件时一定要用PreparedStatement参数占位符不能用字符串直接拼接。很多项目在报表模块加完行级权限后出现安全告警排查下来都是拼接位置用了字符串这块是红线没有商量余地。方法级权限控制我用Spring自带的PreAuthorize(hasAuthority(system:user:add))配合菜单表的perms字段登录时加载到authorities里。生产上这么一套组合下来菜单、按钮、数据三个层级都覆盖到了政务权限模型的三层就齐了。这个方案在Java岗位面试里也是加分项能讲清楚为什么不在Service层过滤的人说明真的处理过权限穿透问题。4. 审批流转与表单用Java自研状态机把事项电子化串起来4.1 选型判断为什么小平台先别上Flowable政务平台里最硬的一块工作是审批流转。一类是内部审批比如盖章申请、公文流转流程相对固定另一类是业务事项审核比如企业资质申报要串多个部门并行会签。用现成的Flowable或Activiti还是自研状态机我见过两边都翻车的项目。用过Flowable的项目往往要独立配置一个建模工程业务人员和开发一起画BPMN图一个两周的交付周期光建模就耗掉三天引擎跑起来内存占用还不小。维度Flowable/Activiti自研状态机学习成本高要理解BPMN、引擎表结构低一张状态表加配置表需求适配会签、条件网关、任意跳转适合固定串行审批分支靠代码兜底排错难度引擎内部表几十张定位靠日志SQL一查流转记录就清楚部署成本独立引擎服务零额外组件选型判断标准是流程复杂度。如果只是发起、部门审批、分管领导审批、办结最多加一个退回重填自研状态机两周就能交付如果需求里有会签、或签、多人顺序审批、限时办结而且需求方明确要流程可配置那再上Flowable。政务小平台按自研做性价比最高我这么说不是否定流程引擎而是见过项目用Flowable搭完最后因为两个节点退回建模和引擎配置改了四版需求方还觉得流程改动应该很简单。4.2 两张表跑通审批状态机的事务边界与驳回处理自研状态机的最小数据模型就两张表flow_instance存当前节点和整体状态flow_record存每一步操作痕迹。CREATE TABLE flow_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, business_key VARCHAR(64) NOT NULL COMMENT 业务单号, current_node VARCHAR(64) NOT NULL COMMENT 当前节点编码, status VARCHAR(16) NOT NULL COMMENT RUNNING/APPROVED/REJECTED/WITHDRAWN, creator_id BIGINT NOT NULL, create_time DATETIME NOT NULL, UNIQUE KEY uk_business (business_key) ) COMMENT流程实例表; CREATE TABLE flow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, instance_id BIGINT NOT NULL, node_code VARCHAR(64) NOT NULL COMMENT 节点编码, operator_id BIGINT NOT NULL, action VARCHAR(16) NOT NULL COMMENT APPROVE/REJECT/TRANSFER, opinion VARCHAR(500) COMMENT 审批意见, create_time DATETIME NOT NULL, KEY idx_instance (instance_id) ) COMMENT审批记录表;审批动作的Service方法核心是状态迁移和审批记录写入必须落在同一个事务里Transactional(rollbackFor Exception.class) public void approve(Long instanceId, Long operatorId, String opinion) { FlowInstance instance flowInstanceMapper.selectById(instanceId); String nextNode flowConfigService.getNextNode(instance.getCurrentNode()); if (nextNode null) { instance.setStatus(APPROVED); } else { instance.setCurrentNode(nextNode); } flowInstanceMapper.updateById(instance); FlowRecord record new FlowRecord(); record.setInstanceId(instanceId); record.setNodeCode(instance.getCurrentNode()); record.setOperatorId(operatorId); record.setAction(APPROVE); record.setOpinion(opinion); flowRecordMapper.insert(record); }参数说明Transactional必须写rollbackForException.classSpring默认只对RuntimeException回滚而政务代码里随手扔个Exception的情况太多了不加rollbackFor就会出现审批记录已经写入、流程状态没变的怪象操作日志和实际流程对不上被审计查到就是事故。nextNode从流程配置表里查流程配置表定义每个节点的下一步编码比如dept_leader_audit的下一步是division_leader_audit查不到下一步就说明流程跑完状态置为APPROVED。驳回场景的处理是走REJECT分支状态置回上一个节点同时插一条actionREJECT的审批记录审批意见必须留痕谁在什么时间驳回的、理由是什么全程可追溯。会签场景先把一个节点的审批人集合查出来给每个人都生成一条待办记录等集合里的人全部提交approve才推进节点这个逻辑用一张todo表就能扩展不必为此上流程引擎。4.3 前端接口约定发起、待办、草稿与Long精度丢失后端至少要向前端提供四个接口发起审批、待办列表、流程详情、审批动作。发起接口有一个重要参数叫businessKey前端在提交前先生成业务单号这样即使表单提交中途失败用同一个businessKey重试也不会生成重复的流程实例。PostMapping(/workflow/instance) public ResultString start(RequestBody Validated StartProcessReq req) { String businessKey req.getBusinessKey(); Long count flowInstanceMapper.countByBusinessKey(businessKey); Assert.isTrue(count 0, 该业务单已在审批流程中请勿重复提交); // 创建流程实例候选审批人列表写入待办表 return Result.success(businessKey); }政务表单页最坑的是用户填了半小时提交时token过期内容全丢。常规做法是草稿箱后端一张草稿表存JSON快照提交时带草稿ID失败后用户从草稿续办这个功能不算KPI但决定了用户愿不愿意用系统。另一个细节是JSON里的大数字Long会被JavaScript丢失精度业务单号这种18位以上的数字要序列化成String返回这是被运维现场点到过的问题政务遗留系统里有多少因为Long精度对不上账的问题做过就知道。注意政务内网大量客户端还是国产化浏览器内核版本停留在Chromium 70到80之间对ES2015之后的语法支持不完整。前端工程如果用了较新的语法特性要在交付前做一次低版本内核兼容测试这个问题在开发机上完全暴露不出来。5. 避坑指南政务项目从部署到国产化的6个常见问题政务平台的坑大多不在业务代码在内网部署环境、国产数据库、浏览器兼容和缓存策略这几个方向。以下几条都是实际交付中被反复点名的每一条都按现象、原因、解决给全。5.1 内网偶发登录失败时间不同步让JWT提前过期现象是本地开发环境登录稳定部署到内网后用户偶发反馈登录失败过几分钟又自己好了。原因有两个叠加内网机器时钟没有完全同步服务器时间比真实时间慢几分钟JWT的exp字段严格按服务器当前时间校验token刚签发就被判定过期。解决是在部署脚本里加上时间同步应用启动参数加-Duser.timezoneGMT8同时JWT解析时设置30秒的clockSkew容忍度这个组合基本能根治。5.2 国产数据库下分页数据异常方言参数要显式声明现象是分页接口在MySQL上正常切到达梦或人大金仓后出现数据重复、缺失甚至分页直接报错。原因多半是分页插件构造时没传DbType默认按MySQL的LIMIT语法拼SQL国产库的方言里LIMIT写法不一样。解决是分页插件写成new PaginationInnerInterceptor(DbType.DM)或DbType.KINGBASE同时把MyBatis-Plus升到较新版本旧版本对国产库的方言支持确实粗糙这个位置报错经常被当成玄学其实是参数问题。5.3 同浏览器登录多账号互顶Redis会话键设计错误现象是同一台电脑用两个账号测试后登录的账号会把先登录的顶下线。原因是用Redis存会话映射时key设计成user_token:username这种固定格式后登录的这条记录覆盖了旧的。解决是token的主键用随机UUID另存一份user_token:userId指向当前有效的token要互踢时删掉这份映射即可。还有一个安全点token串不能用userId当唯一标识容易被遍历生成安全测评会专门查这个。5.4 导出Excel中文文件名乱码响应头缺filename*现象是导出列表时文件能下载但文件名是乱码Linux服务器上百分号编码Windows上问号。原因是Content-Disposition头只给了filenameutf-8中文老浏览器不认。解决是响应头写两段filenamexxx.xlsx的ASCII回退值再加filename*UTF-8百分号编码的中文名这个两段式写法是被多种浏览器实测过的两行代码改完能少一个现场返工。5.5 改完权限不生效Redis权限缓存没主动失效现象是管理员在后台调整角色权限后用户端必须重新登录才能看到变化催得紧的时候会被当成权限没改。原因是用户权限在登录时一次性加载进Redis权限变更只更新了数据库缓存没有失效。解决是在角色管理的更新接口里事务提交后执行Redis的keys删除操作把该角色下所有用户的权限缓存清掉让下一次请求重新加载。注意删除动作要放在事务提交之后放在事务里万一回滚缓存被清但数据没变全部用户都被迫重新登录。5.6 操作日志对不上账同步写日志拖垮主链路现象是审计抽查时发现部分操作日志缺失或者业务高峰期接口变慢进一步排查发现日志表和业务表同时写入日志表慢SQL拉低了整个接口。原因是日志写入放在业务事务里同步执行。解决是把操作日志用Spring AOP采集放进内存队列异步落库失败降级只打应用日志不做业务重试。日志表字段设计时把审批意见这类文本定长截断不能把500字塞进VARCHAR(100)导致插入失败这个细节在验收抽检时很容易被发现。6. 交付前最后一步用一条审批链路验证设计6.1 验收链路登录、授权、审批、留痕四步验证工程跑起来以后别急着点菜单先按真实业务流模拟一条完整链路管理员创建两个角色一个区县办事员一个市局审批人分别指派给两个测试用户。然后按顺序断言四件事。步骤操作预期结果1办事员登录只看到自己角色的菜单验证码失效不能复用2办事员发起事项审批流程实例表新增一条记录状态为RUNNING3切换审批人登录待办列表出现该事项行级权限外的数据查不到4审批人同意并填写意见流程状态流转记录表新增APPROVE记录日志留痕可查这条链路把表结构、权限模型、工作流、日志审计全部串起来验证。哪一步卡住排查顺序是先看JWT过滤器有没有放行再看数据权限拦截器拼出来的SQL条件对不对最后看事务里的状态更新有没有提交。链路通了项目的基本盘就稳了。6.2 如果还有一周优先做表单动态建模政务业务变化最快的就是表单。今天加一个字段明天改一个下拉选项每次都改实体、数据库、前端页面交付周期被拖垮。常见做法是给业务表预留一个JSON扩展字段把动态字段的快照存进去前端按JSON结构渲染表单后端只校验必填项。动态建模不必一开始做完整产品先支持文本、数字、日期、下拉、附件五种控件就能覆盖九成以上的政务小事项这是比加新页面更划算的投入。我做这类项目踩得最深的一次是表结构交付前被需求方改了六版起初每改一版都是全链路返工后来引入扩展字段才止损。这件事给我的教训是政务平台设计源码的价值不在于代码多炫而在于权限、流程、留痕这三根梁没有歪。架构选型守住单体底线权限模型用在SQL层审批留痕走独立记录表剩下的功能都是往上填砖。希望帮到你。本文还有配套的精品资源点击获取
返回列表