接政务项目的朋友应该都有这种感觉:需求文档里写着“符合等保三级要求”、“权限分权分域”、“审批流程灵活可配置”,乍一看不过是三个标准功能模块,可真开工后才发现,这三个词每一个背后都是一整套工程体系,凑到一起更是处处暗坑。
我这两年做过几套市级的行政审批、OA办公类系统,用的就是Spring Boot全家桶。今天这篇不聊框架的基本用法,专门把等保合规落地、权限管控建模、工作流集成这三块串起来讲,讲讲每个模块怎么设计、怎么实现、模块之间怎么协同,以及上线前被等保测评机构卡脖子时最容易被拎出来问的几个点。适合正在做或准备做政务类系统的Java后端开发参考。
1. 等保三级约束下,Spring Boot项目最先要动的基础设施
等保测评落到应用层面,最大的感受是:它不考核你的技术栈是不是最新,也不看你用没用微服务,它按条款一条条对照检查,身份鉴别、访问控制、安全审计、数据保密性每一项都有硬指标。设计Spring Boot政务项目时,这几点得在写第一行业务代码之前就考虑好。
1.1 身份鉴别:登录防爆破和会话控制必须做到位
普通商业后台的登录逻辑往往是用户名密码校验通过就放行,但等保场景下登录环节是测评重点。我接手过一个项目,第一版连失败锁定都没有,测评意见直接列为高风险项。等保三级对身份鉴别最直接的几个要求是:用户身份唯一标识、登录失败处理(锁定策略)、登录超时退出、双因素认证(对政务系统来说这是加分项但多数地区已经是硬要求,至少要求二次验证或CA证书登录)。
我的落地做法是:密码策略做成可配置项,配置表里有密码最小长度(默认10位以上)、必须包含字符类型(大小写字母、数字、特殊字符)、密码有效期(比如90天强制修改,到期前7天提醒)、历史密码不重复次数(默认5次内不得重复)。登录失败锁定策略用Redis做计数器,5分钟内失败5次锁定账号30分钟,锁定后即使密码正确也拒绝登录,管理员可以在后台手工解锁。这些策略全部做成配置项而不是写死在代码里,原因很现实:测评机构会针对每一条配置逐项要截图和说明,你做成可配置的,整改时改配置就能过,不用改代码重新发版。
会话控制也需要注意,Spring Security默认的session机制不做空闲超时的话,用户挂着不操作可能一整天都不掉线。我统一在Security配置里加上了sessionManagement,设置60分钟无操作即失效,同时限制了同账号并发会话数,默认只允许一个会话,新登录会踢掉旧会话。政务内网系统里经常有账号多人共用的情况,限制并发会话在等保条款里属于“会话管理”相关检查点,一开始用户会抱怨,但测评通过后大家都能理解。
1.2 安全审计:日志留痕不是打印几条logback记录
很多团队理解的安全审计就是logback里写几行INFO日志,真到等保测评环节,测评机构要求的是“操作可追溯”,即谁、在什么时间、从哪个IP、对哪个业务对象、做了什么操作、操作前后数据变化如何,都要能查出来。单靠文本日志无法满足这种审计要求,要做独立的安全审计表。
我习惯设计一张审计日志表,字段大致包括:
| 字段 | 说明 |
|---|---|
| audit_id | 主键 |
| user_id / user_name | 操作人 |
| ip_address | 来源IP |
| operate_time | 操作时间 |
| module_name | 模块名,如“用户管理” |
| operate_type | 操作类型:登录、登出、增、删、改、查、授权、审批 |
| request_url / method | 请求路径和HTTP方法 |
| req_param | 关键入参,脱敏后的JSON字符串 |
| result_code | 操作结果,成功或失败 |
| fail_reason | 失败原因,如“密码错误”“无权限” |
审计日志的写入不要用业务代码手动一条条记,而是用AOP切面拦截Controller或Service层的方法,结合自定义注解做自动记录。比如我写一个@AuditLog(module = "user", action = "insert")注解,切面里解析注解、抓取登录用户上下文、序列化参数后异步写入审计表。这里有两个经验:第一,参数序列化必须做脱敏,密码、身份证号、手机号等敏感字段统一替换成占位符,否则审计表本身就变成了数据泄露源;第二,写入审计日志要异步化或用独立事务,避免审计操作失败导致主业务回滚。等保三级通常要求日志留存不少于6个月,所以审计表要配合归档机制,按月分表或定期导出。
1.3 数据保密性和国密算法改造
政务系统对敏感数据的保护要求很明确:重要数据加密存储、敏感字段脱敏展示、传输信道加密。我处理的范围一般包括用户的身份证号、手机号、家庭住址、银行账号等,落地方式是统一封装一个敏感字段处理器,利用MyBatis的TypeHandler,在写入时自动加密,JSON输出时自动脱敏。加密算法上,新做的政务项目建议直接用国密SM4,虽然可以选用AES,但很多地区的等保测评细则里已经将国密算法列为符合项,选用SM4能少很多解释工作。
国密改造容易忽略的是密钥管理,密钥不能写在配置文件里硬编码。我一般用独立的密钥服务中心或在环境中注入密钥,JVM启动时从环境变量加载。SM4加密是分组对称加密,注意选择合适的填充模式,推荐SM4/ECB/PKCS5Padding,如果对安全性要求更高可以用CBC模式并保存随机IV。这里补充一句:用户密码不要用可逆加密,无论等保还是《数据安全法》相关的落地检查,密码哈希都是底线。我在项目里用BCrypt做密码哈希,如果要过国密适配,可以用SM3加盐替代,但需要自己处理好盐值的存储和校验逻辑。
2. 权限管控落地:RBAC建模、动态拦截和SQL级数据过滤
权限管控是政务系统另一个重头戏。等保里的“访问控制”条款要求最小权限、权限分离,而业务上政务系统往往有复杂的组织层级,市、区、街道、社区,每一层的数据可见范围都不同,权限设计要从功能权限和数据权限两个维度同时考虑。
2.1 核心表结构设计:从五张表到可扩展模型
我采用的仍然是经典的RBAC模型,但扩展了部门层级。最核心的五张表是用户表(sys_user)、角色表(sys_role)、菜单/权限表(sys_menu)、用户角色关联表(sys_user_role)、角色菜单关联表(sys_role_menu),再加上机构表(sys_dept)用于数据权限。
这里有三个设计要点:
第一,权限表的类型字段要能区分三种权限:菜单权限(控制用户能看到哪些页面)、按钮权限(控制页面内哪些按钮可用)、接口权限(控制API能否访问)。我习惯在sys_menu表中用permission_type字段区分,菜单权限存的是路由地址,按钮权限存的是形如system:user:add的权限标识,接口权限直接关联URL模式。这样前端可以根据按钮权限控制操作入口,后端在接口上做同样标识的鉴权,前后端权限标识保持一致,避免出现“页面无按钮但直接调API能绕过”的问题。
第二,角色与用户的关系建议保留多对多。政务系统中一个人经常身兼多职,比如既做普通审批又兼职系统管理,多对多模型比一对多灵活。但要注意等保里的最小权限原则,给用户分配角色时做互斥校验,同一用户不能同时拥有“系统管理员”和“安全审计员”这类职责冲突的角色,后面讲三员管理时会展开。
第三,超级管理员不要写死在代码里。很多框架默认强制放行id为1的用户,这在政务项目里非常危险。测评人员会专门检查是否存在绕过权限校验的后门。我建议把超级管理员也当作普通用户身份放入角色体系,只是初始数据里给它分配一个“拥有全部权限”的角色,而不在鉴权代码里做任何userId特殊判断。
2.2 Spring Security动态权限拦截:让权限配置进数据库
Spring Security默认的@PreAuthorize("hasAuthority('xxx')")在权限标识写死在注解里的场景下够用,但政务系统的权限是管理员在后台维护的,新增一个按钮权限不能要求开发改代码发版。所以需要把URL与权限的映射关系动态化,存到数据库。
我的做法是自定义一个FilterInvocation级别的授权管理器。核心逻辑是:
项目启动时(或缓存Redis后),从sys_menu表加载所有“接口权限”记录,构建出
Map<String, Set<String>>,key是URL模式,value是该URL需要的权限标识集合。用户登录后,从用户的角色集合中计算出该用户拥有的全部权限标识列表,存到SecurityContext或Redis中。
每次请求到来,根据当前请求的URL找到对应需要的权限标识,判断用户拥有列表中是否包含。
这样实现之后,权限配置全在后台页面上操作,数据库里改一条记录,刷新缓存后立即生效。我给出的核心代码如下:
@Service public class DynamicPermissionManager { @Autowired private SysMenuService menuService; // key: url, value: required permission codes private Map<String, Set<String>> urlPermissionMap = new ConcurrentHashMap<>(); @PostConstruct public void loadUrlPermissionMap() { // load from database } public boolean checkPermission(String url, Collection<String> userPerms) { Set<String> requiredPerms = urlPermissionMap.get(url); if (requiredPerms == null || requiredPerms.isEmpty()) { return true; // 未配置权限要求的接口默认登录后可访问 } return userPerms.stream().anyMatch(requiredPerms::contains); } }这个设计有一个比较重要的细节:URL匹配不能用简单的字符串equals,因为RESTful接口通常带路径变量,比如/api/approval/123匹配/api/approval/{id}。建议使用Spring的AntPathMatcher或PathPattern来做模式匹配,我在实际项目中用AntPathMatcher,简单稳定。
2.3 数据权限:让不同层级的人看到不同范围的数据
功能权限解决“能不能进这个页面”的问题,数据权限解决“进到页面后能看到哪些数据”的问题。这句话是政务系统和普通企业系统最明显的分水岭,也是评审专家最爱考的点。
政务场景下数据权限最常见的规则是数据按部门/行政区划隔离。比如一个统计报表,省厅用户能看到全省数据,市局用户只能看本市数据,区县用户只能看本区县数据,街道用户只能看本街道数据。如果业务代码里每个SQL都手动拼WHERE dept_id = ?,不仅代码重复严重,还容易漏。
我采用MyBatis拦截器实现统一数据权限过滤。核心思路是:拦截所有Mapper的执行,在SQL执行前解析出当前登录用户的数据权限范围,自动拼接过滤条件。拦截器实现代码如下:
@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. 获取当前登录用户的部门路径和可见范围 UserContext user = UserContextHolder.get(); if (user == null || user.isSuperAdmin()) { return invocation.proceed(); } // 2. 解析原生SQL,追加数据权限条件 MappedStatement ms = ... String sql = ... String filteredSql = appendDataScope(sql, user.getDeptPath()); // 3. 用属性反射替换boundSql中的sql return invocation.proceed(); } }拦截器方案的优点是业务代码零侵入,缺点是SQL解析和改写需要小心,遇到子查询、JOIN、group by时需要保证条件插入位置正确。我的经验是:数据权限过滤的目标表统一使用带业务“归属单位编码”的列(比如dept_code),过滤条件统一添加在SQL最外层,并在多表JOIN场景下明确过滤哪个表。如果项目工期紧,也可以退一步只在Service层用一个自定义数据权限工具类手动加条件,但长期维护起来会非常痛苦。
2.4 三员管理:政务涉密系统的一道分权红线
做政务项目,一定会碰到“三员”这个词。三员指的是系统管理员、安全保密管理员、安全审计员三种角色,三者权限互斥、相互制约,避免一个管理员既管系统又管审计从而“监守自盗”。这是等保测评的常见加分项,在一些涉密程度高的单位甚至是强制项,我在公共资源交易、行政审批类项目里都是按这个模型设计的。
三员管理落到权限设计上很简单:建立三个默认角色,明确规定各自的权限边界。系统管理员负责用户维护、角色授权、流程配置;安全保密管理员负责安全策略配置、密码策略修改;安全审计员只拥有查看所有审计日志的权限,不能修改系统配置也不能授权。这里有个容易踩的坑:审计员必须能查看日志,但查看日志这个功能本身也是敏感操作,设计时要保证审计员看日志的行为也被记录,也就是“审计的审计”。我的做法是审计日志查询接口也走AOP审计切面,审计员打开日志列表页面时同样落一条日志。
三员互斥还要落在数据库层面,而不是只在界面层不让勾选。用户分配角色时做后端校验,如果新角色和已有角色互斥,直接拒绝保存。交互上可以做提示,但真正的控制必须放服务端,这个习惯无论等保测评还是安全专家的代码审计都能用上。
3. 工作流选型与集成:Flowable在政务审批里的实战经验
政务系统最典型的工作流场景就是审批,公文审批、事项审批、拨款审批、立项审批,流程千奇百怪但又高度相似:谁发起、经过哪几个节点、每个节点有哪些审批人、不同条件下走不同分支。市面上开源工作流引擎不少,我用的最多的是Flowable,下面讲讲为什么选它、怎么集成的,以及几个关键节点的实现思路。
3.1 选型对比:为什么是Flowable而不是其他引擎
老牌的Activiti、后来的Camunda、国产的盘古BPM,以及完全自研的审批流,我都调研过。最终在Spring Boot政务项目里固定用Flowable,理由是直接的:
- Java 8兼容性好。政务系统所在内网环境JDK版本往往保守,Flowable 6.x完美运行在Java 8上,Camunda 7.x虽然也兼容但配置组件更重。
- BPMN 2.0标准支持完整,流程设计器最成熟。设计好的bpmn文件可以非常方便地部署到生产环境。
- 和Spring Boot集成成本低,提供了starter,数据源可以和业务库分离或共用,事务交由Spring管理。
自研审批流我劝大家慎重。审批流程最具迷惑性的一点是:看起来就是一张表记录“谁提交谁审批”,但真正做起来会发才是“状态机+参与者计算+驳回跳转+历史版本兼容”四件事的集合。尤其是会签、驳回、会办这些政务场景高频操作,自己写很难覆盖全面。我的态度是:有标准就不要重复造轮子,Flowable这类成熟引擎唯一的成本是学习曲线,但这个成本花得值。
3.2 引擎集成Spring Boot:三步跑通第一个流程
第一步,引入依赖。Flowable 6.x用官方提供的spring-boot-starter即可:
<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.7.2</version> </dependency>第二步,配置数据源和应用配置。我建议Flowable引擎表(ACT_开头的那一堆表)和业务表放在同一个库中,这样可以利用同一个事务管理器。配置上需要关闭自动部署或指定流程文件目录:
flowable: database-schema-update: true async-executor-activate: false process-definition-location-prefix: classpath*:/processes/这里有一个要点:async-executor-activate这个参数,政务内网环境如果不需要异步任务,建议关掉,避免多余线程在有些安全加固后的内网环境被误判为可疑进程。另外流程文件目录统一放在processes目录,启动时自动部署新版本的流程定义,这样的行为在测试和预发、生产环境一致,稳妥。
第三步,部署和发起一个流程。在Flowable中,部署流程定义是读取bpmn文件,发起流程是让流程实例按照定义流转。示例代码如下:
// 部署流程定义(系统启动时自动完成,也可手动调) repositoryService.createDeployment() .addClasspathResource("processes/leave-approve.bpmn20.xml") .name("请假审批流程") .deploy(); // 发起流程实例,processKey是流程定义中的id Map<String, Object> vars = new HashMap<>(); vars.put("applicant", user.getId()); vars.put("days", 3); ProcessInstance instance = runtimeService.startProcessInstanceByKey("leaveApproval", vars);发起流程的时候流程变量会伴随整个流程实例生命周期,后面的分支条件判断、审批人计算都会用到这些变量。
3.3 会签、或签、条件分支和驳回到任一步
政务场景里最常被问到的四个流程能力,我这里逐个说。
会签,指需要多个审批人同时审批,全部通过才能继续流转。Flowable中用多实例节点实现,在bpmn文件里配置multiInstanceLoopCharacteristics,type为parallel即并行会签。审批人集合先在流程变量里算好,节点上配置activiti:collection="${approverList}"和activiti:elementVariable="approver"。每个审批人的审批结果是单独的任务,通过与否累计判断,全部agree才流转到下一节点。
或签,多个审批人中任意一人通过即流转。实现时把多实例类型改成sequential或者parallel,但完成条件配置为nrOfCompletedInstances >= 1即可,即在第一人完成后直接结束多实例。
条件分支是工作流最灵活的地方,典型的如报销金额超过5000元需要走总经理审批,否则部门经理审批即可。在bpmn的排他网关上配置条件表达式:
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression"> ${amount >= 5000} </bpmn:conditionExpression>流程变量里的amount在发起时传入。这个表达式的计算发生在流程流转那一刻,所以变量传递的时机非常重要,变量必须在到达排他网关之前就放进流程作用域,否则表达式取不到值会直接抛异常。我遇到过生产环节因为变量名拼写不一致导致表达式一直按false走,排查了半天才发现是前后定义变量名不统一,用map传变量时建议统一管理变量名常量类。
驳回到任一步,这个需求在政务系统里几乎是必须项——审批人觉得材料有问题,要退回给发起人修改,甚至退回给上上环节重新核验。Flowable的原生实现方式有两种:一种是通过ChangeActivityStateBuilder来跳转,另一种是在流程设计阶段预留驳回网关。我更推荐后者,在流程图中显式画出“驳回”分支,走一条连到目标节点的人工流转,因为这样流程定义一目了然,而且审批记录在BPMN历史上是连贯的,等保审计查询时能完整还原轨迹。前者更适合管理端的“强制跳转”,比如管理员介入把卡死的流程跳到某个节点继续执行。
驳回之后的处理要特别注意:驳回后流程实例回到目标节点,新的待办任务生成,原审批人的历史任务保留,但业务表状态要跟着流程节点变。我在审批通过和驳回任务监听器里都维护业务表状态字段,保证“流程进行到哪一步、业务数据状态就是什么”。这块如果没做好,用户会看到流程已经驳回了,但业务记录还停留在“审批中”,就是典型的流程与业务状态不同步。
3.4 流程与权限、审计的协同设计
工作流模块独立开发不难,难的是和权限、审计打通。我总结出三个必须处理好的协同点。
第一,发起权限校验。不是所有人都能发起流程,要有对应流程的申请权限。我的做法是每个流程定义一个“发起权限标识”,用户在发起流程时后端实时校验用户是否拥有该标识,没有则直接抛异常。这个校验不能只依赖前端隐藏菜单,因为接口是公开的,直接调接口就能绕过页面限制。
第二,审批人计算。审批人不是发起时写死的,而是每个节点动态计算。我在用户任务监听器里根据当前节点配置的候选组角色,查询角色下在指定部门范围内的在线用户,生成待办任务并指定给具体用户。这里用角色而不是具体人,好处是人员调整时不需要改流程定义。政务系统经常有借调、轮岗情况,角色化指定审批人后,流程定义稳稳不动。
第三,流程操作的审计。等保要求流程审批的关键操作全部可追溯。我在Flowable的全局监听器里监听任务创建、任务完成、流程结束等事件,把操作人、审批意见、审批时间、节点信息写入审计表。这里强烈建议用Flowable内置的事件监听机制,而不是在各处业务代码里手工调审计接口,否则流程引擎内部走了异步路径,手工日志根本覆盖不全。
4. 三大模块融合时最容易翻车的场景
把等保合规、权限管控、工作流都做出来了,并不意味着系统就能稳稳上线。三个模块各自独立时都正常,一旦组合运行,有些问题就冒出来了。下面这几个场景,每一个我都真实踩过或见证同事踩过。
4.1 事务边界问题:流程提交和数据落库不一致
最典型的翻车场景是:审批流走到了“通过”,但业务表没有更新。原因是把runtimeService.complete(taskId, vars)和业务表更新放在一个事务里,Flowable的命令执行有自己的事务传播行为,如果业务更新先执行但流程完失败,业务数据可能被回滚,而流程实例的推进可能已经提交。
我用的稳定做法是:流程推进和业务更新都交给Spring管理,用@Transactional整体包住,顺序上先更新业务表再complete。同时利用Flowable的invoke回调,在流程推进失败时整体回滚。如果项目允许稍微降低一点一致性,也可以用最终一致思路,流程成功后再通过监听器更新业务表,但这会引入“流程已通过但业务状态短暂不一致”的窗口期,政务系统对状态一致性要求高,我强烈建议走强一致路线。
4.2 权限变更之后,进行中的流程实例怎么处理
用户A在第2节点审批,此时管理员把A的权限收回了,流程实例还能不能继续走?这个问题在产品设计阶段就要回答,不然上线后会被业务方反复投诉。我的方案是:流程实例启动时,把每个节点的潜在审批人集合以及发起人信息作为流程变量快照存入流程实例;后续任务分配时优先取快照中记录的审批人,而不是实时查当下角色关系。这样权限变更只影响新发起的流程,已经走了一半的流程实例保持原审批人。审批人调走的情况,由安全保密管理员在管理后台手动改派该流程实例当前待办人,这是符合政务实操需求的兜底设计。
同时也要考虑角色删除时对流程定义的影响。如果流程配置里的候选组引用了一个被删除的角色ID,后续发起流程会找不到候选人,直接报错。所以流程定义引用角色时建议校验角色是否有效,删除角色前检查它是否被流程定义引用,被引用则在界面上提示“该角色正在被流程使用,不可删除”,改为逻辑禁用。
4.3 等保测评时机构重点检查哪些应用层面的点
做政务项目最终都要过等保测评,测评不只是看物理机房的防火墙,应用层面的检查几乎全都集中在本文提到的三个模块。我参加过的等保测评中,测评人员最常做的操作包括:
第一,尝试弱口令和暴力破解。他们会用测试工具对登录接口做多次失败尝试,检查系统是否锁定账号、是否触发告警。这个在第1节说过,一定要有实现并且保留测试过程产生的日志。
第二,检查越权访问。比如用普通用户身份直接访问管理员的API接口,看后端是否返回403或者重定向。这直接对应第2节的动态权限拦截。我在自测阶段会用两个账号分别登录,把每个REST接口过一遍,确认权限标识配置完整,接口没有被遗漏放行。
第三,仔细翻看权限配置页面。测评机构会查看管理员是否有多余的高权限账号、是否有弱口令账号、安全审计员是否能查看所有日志。我建议上线前做一次账号清理,把测试账号、初始密码账号全部禁用或修改,给测评留一个好印象。
第四,检查数据加密。测评人员会查看数据库里的身份证号、手机号字段是否明文存储。不及格的结果往往是看到了明文。所以敏感字段加密这部分,测试环境就应当用真实逻辑,不能临时跳过,否则测评当天会现场翻车。
第四,检查操作日志能否留存并能查询。他们会随机抽查几个操作记录,看审计日志表里能不能查到对应记录,以及日志是否只增不删(数据库层面做权限限制,普通管理员无删除权限)。
4.4 上线部署前的安全检查清单
最后分享一份我每次交付政务项目前都会跑一遍的检查清单,按测评高风险项整理的,可以拿去直接用:
| 检查项 | 具体要求 | 对应章节 |
|---|---|---|
| 弱口令 | 管理员账号密码强度配置生效,测试账号清零 | 1.1 |
| 登录防爆破 | 失败锁定策略生效,Redis计数正常 | 1.1 |
| 会话控制 | 空闲超时、并发会话限制生效 | 1.1 |
| 审计日志 | 登录、增删改查、授权、审批都有记录 | 1.2 / 3.4 |
| 敏感数据 | 手机号、身份证号等加密存储,日志脱敏 | 1.3 |
| 权限分配 | 最小权限,三员互斥,超级管理员不越权 | 2.4 |
| 数据权限 | 跨部门访问测试通过,无越权看数据 | 2.3 |
| 流程状态 | 业务表状态与流程节点一致,驳回后状态正确 | 3.3 |
| 接口防护 | 未授权的URL请求返回403,不泄露敏感信息 | 2.2 |
这套检查做完,等保测评至少不会因为基础性的低级问题被打回。再提醒一句:测评时拿出来的材料要和你代码里实际实现的逻辑完全一致,演示环境用真实配置,不要打插边球,政务项目里弄虚作假的风险谁都不想背。
政务系统的开发本质上是在“标准框架的约束下做最稳妥的工程选型”。等保合规要求你每一步都留痕,权限管控要求你每一处访问都可控,工作流要求你每一个节点都清晰。这三个模块交织在一起,就构成了政务系统最核心的底座。做这一行的价值正在于此:在严格的规范里做出好用的产品,每一次过测评、每一次业务方验收通过的背后,都是工程能力在兜底。希望这篇实战梳理能给正在做同类项目的朋友一点参考。