第三部分:安全管道
第十一章 政务安全管道——不靠安全框架,靠管道设计
本章核心:六道安全防线全做在框架管道里——绑定变量、SM4加密、SM3签名、commit审计、数据层级控制、操作权限控制。
前面十章讲了框架的数据流转管道——从route.java入口,经过IOC、AOP、DBUtil,最终到达数据库。这一章要讲的是:这条管道不只传输数据,它同时过滤安全威胁。
安全做在管道里,不做在业务里。 这是框架设计中最容易被忽略但影响最深远的决策。
11.0 六道防线,一条管道
等保测评组来了。要求出示:SQL注入防护方案、敏感数据加密方案、操作审计方案、防篡改方案。
我们没有买任何安全框架。没有WAF,没有Spring Security,没有Shiro。
但四项要求全部满足。还有两项是测评没提但我们自己加的:数据层级控制和操作权限控制。
答案只有一句:每一层管道都过一遍,安全就成了"副产品"。
11.1 第一道防线:SQL注入防御——绑定变量从管道层面强制
SQL注入是Web安全的老大难问题。防护的核心原则所有人都知道:用绑定变量(#{}),不用字符串拼接(${})。
问题在于:靠人遵守规范不如靠机制强制。 几百个MyBatis Mapper文件,上千条SQL,总有人偷懒用${}。而且政务系统的开发团队通常是分散的——有的是我们的人,有的是客户的人,有的是外包团队。你不可能培训所有人、检查每一条SQL。
我们的方案:从框架管道层面强制保障。
第九章讲过改MyBatis源码一行实现绑定变量物理分页。这个改动的安全意义比性能意义更大——分页参数通过Dao的maxRow/minRow字段传递,PaginationInterceptor拦截后把它们作为绑定变量追加到SQL的?占位符。只要走DBUtil管道,分页参数就不可能被拼进SQL字符串。
进一步,我们的规范要求所有查询参数必须通过#{}传入。但这还不够——规范是给人看的,人会犯错。所以框架在DBUtil层做了一个检查:getDao()执行前扫描SQL模板,如果发现{},比如动态表名),而是留痕——出了问题可以追溯。
这不是一个完美的SQL注入防御方案。但在政务场景下,它足够务实:不依赖人的自觉,在管道层面提供兜底。
11.2 第二道防线:敏感数据加密——SM4注解驱动
第十四章详细讲了SM4加解密的设计。这里从安全管道的视角重新审视。
加密的安全收益显而易见:数据库里存的是密文,即使数据库被拖库,敏感数据也不会泄露。但加密方案的设计方式决定了它的安全上限——加密做在业务层,遗漏是必然的;加密做在管道层,遗漏是不可能的。
为什么?因为所有数据操作都经过DBUtil管道。SaveDao()自动调Mac()和enCode(),getDao()自动调deCode()和verMac()。业务代码不需要"记住"要加密——只要Dao字段上标了@myCode,管道自动处理。新人加入项目,只要走DBUtil管道,加密就是自动的。
对比:如果加密做在业务层——每个Service方法自己调加密服务。几百个方法,漏一个就是漏洞。政务系统的开发团队是分散的,你确定每个人都不会漏?
usageCode全局开关是另一层安全设计。同系统部署在不同省份,有的要求加密有的不要求。不加密的省份usageCode=0,enCode()直接return——等于零风险。想加加密?改一行配置就行,不需要改代码、不需要回归测试。
11.3 第三道防线:防篡改——SM3签名验签
加密解决了"数据泄露"的问题,但没解决"数据篡改"的问题。如果有人在数据库里直接改了密文(有数据库访问权限的内鬼),加密系统是检测不到的。
@myMac注解和verMac()方法解决的就是这个问题。
写入时:Mac()把带@myMac注解的字段值Base64拼接,调密码网关算SM3摘要,签名存到Dao基类的mac字段。
读取时:verMac()拿明文重新算签名,和存储的签名比对。不匹配就抛异常。
这意味着:哪怕有人在数据库里直接改了密文,验签也会发现。 因为改了密文,解密后的明文就变了,明文变了签名就不匹配。除非攻击者同时拥有SM3的密钥、知道Base64拼接的顺序、能重新算签名——这在密码网关独立部署的架构下几乎不可能。
先加签再加密的顺序也是安全设计。第六章讲过:签名算的是明文的摘要。如果先加密再算签名,签名保护的是密文的完整性,而不是明文的完整性——密文被篡改后解密出的明文可能是乱码,也可能是有意义的伪造数据。先算明文签名,再加密,验签时先解密再验签,才能确保"明文+签名"的端到端一致性。
11.4 第四道防线:操作审计——commit里原子写日志
第六章讲了BusinessSession.commit()先写审计日志再提交事务的实现。从安全管道的视角看,审计日志的价值在于:任何数据变更都有完整的操作记录,满足等保测评的审计要求。
11.5 XSS 防御的意外收获
框架有两层XSS防御。第一层是第五章的统一入参解析——praserInParameter()从JSON解析参数,参数值是作为数据传递的,不是HTML片段。加上前端用Dojo组件库通过DataStore绑定数据而非直接操作DOM,XSS攻击面被大幅压缩。
第二层更直接——全角字符替换。XssHttpServletRequestWrapper在EncodingFilter里套到每个请求上,拦截getParameter()和getHeader(),把6个危险字符替换成全角:
| 半角 | 全角 | 危险场景 |
|---|---|---|
| < | < | HTML标签注入 |
| > | > | 闭合标签 |
| ' | ' | JS单引号注入 |
| " | " | JS双引号注入 |
| \ | \ | 路径穿越、转义绕过 |
| # | # | URL片段注入 |
全角字符浏览器不解析为标签或脚本,显示效果和半角几乎一样。为什么不用HTML转义(<→<)?政务环境下数据在浏览器、政务外网、内嵌IE控件等各种终端展示,HTML转义在老版本IE上显示异常。全角字符没这个问题。&没有替换——政务系统里出现频率太高,"张三&李四"、"XX局&YY处",替换了影响正常显示。实测六个字符已挡住绝大多数XSS。
整个类106行,无第三方依赖,就是一个switch-case。不是理论上的完美防御,但够用、稳定、好排查。
11.6 第五道防线:数据层级控制——连接里塞机构ID,视图里自动过滤
前面四道防线防的是"坏人"。还有一类安全问题防的是"好人看错数据"——县级用户只能看本县,乡镇只能看自己,省里才能看全域。
这不是菜单权限能解决的。菜单权限控制的是"你能不能进这个页面",数据层级控制的是"你进了这个页面能看到哪些数据"。同一个参保人查询页面,省里看到全省几百万条,县里只看到本县几十万条,乡镇只看到本乡镇几千条。
怎么做?框架搞定,业务代码无感。
第一步:Oracle数据库建视图。以机构表op_unit为例:
create or replace view v_AB01 as
select "UNIT_ID","UNIT_NAME","UNIT_DESC","PARENT_ID","AAB034",
"AAE011","AAE036","AREA_NO","BAZ001",AAF019
from op_unit
start with unit_id = USERENV('CLIENT_INFO')
connect by prior unit_id = parent_id;
USERENV('CLIENT_INFO')是Oracle的会话级变量。START WITH从当前用户的机构开始,CONNECT BY PRIOR往下遍历所有子机构。县级用户登录,视图自动只返回本县及下属乡镇的数据。
第二步:框架在每次请求取数据库连接时,把session中的unit_id塞进CLIENT_INFO。为什么每次?因为连接池的连接是复用的——上一次请求是县级用户,下一次可能是乡镇用户,不覆盖就串数据了。
第三步:业务代码查的是视图v_AB01,不是原表op_unit。该写SQL写SQL,该用MyBatis用MyBatis,不用加任何WHERE条件。视图自带过滤。
三步加在一起:登录时session里存unit_id,每次请求取连接时框架塞进去,业务表关联视图自动过滤。业务代码一行不用改,数据层级控制全在管道里完成。
11.7 第六道防线:操作权限控制——数据库配权限,按按钮id隐藏
数据层级控制解决了"能看到什么数据"。还有一个问题:"能做什么操作"。
不是菜单权限——菜单权限控制的是"你能不能进这个功能"。操作权限控制的是"你进了这个功能,页面上哪些按钮能用"。比如同一个待遇核定页面,科长有"审批"按钮,科员只有"查看"按钮。不是灰掉,是直接看不到。
怎么做?也很简单——数据库里配。
每条权限记录就是"角色+按钮id"的组合。运行时框架查询当前用户的角色,把所有有权限的按钮id取出来。没有权限的按钮呢?根据id,invisible。前端拿到的就是"这几个按钮不要显示"的列表,按id隐藏。
这种方案不漂亮,但管用。按钮id是前端定的,权限配置是运维人员在管理后台加的,开发不用改代码,运维不用找开发。加一个新角色?数据库插几条记录。某个角色的权限要调?改配置数据。
11.8 管道设计的安全收益
安全做在管道里意味着三件事:
业务开发人员不需要"记住"要安全。 走DBUtil管道,加密、签名、审计自动生效。新人不需要安全培训,框架就是安全培训。
安全策略升级只改管道不改业务。 比如从SM4换到SM2,只改enCode()/deCode()两个方法。几百个业务模块零改动。如果安全做在业务层,几百个方法里的加密调用全部要改。
遗漏的概率趋近于零。 管道是统一入口,所有数据都过一遍。不像业务层,每个方法自己处理——漏一个就是漏洞。
对比一下"管道安全"和"业务安全"的维护成本:
| 维度 | 管道安全 | 业务安全 |
|---|---|---|
| 加密遗漏 | 不可能(管道自动处理) | 必然(几百个方法漏一个) |
| 安全升级 | 改管道,业务零改动 | 每个方法都改 |
| 新人上手 | 不需要安全培训 | 需要培训+代码审查 |
| 安全审查 | 审查管道即可 | 审查所有业务代码 |
11.9 为什么不买安全框架
政务系统有等保要求,安全框架当然存在——Spring Security、Apache Shiro,功能强大、生态成熟。
但我们的场景:
• 安全需求明确(加密、签名、审计、防注入),不需要通用的安全框架
• 引入Spring Security增加几十个jar包的依赖、复杂的配置、版本兼容问题
• 框架的管道设计已经覆盖了核心安全需求
• 买了框架不代表安全——真正安全是每一层数据流都经过处理,不是装了一个安全网关就万事大吉
有人会问"你们没有权限控制吗?"有。但权限控制做在route.java的登录校验里(第五章讲的),不在安全框架里。政务系统的权限模型简单——基于角色的菜单权限,session里存角色,route.java根据角色判断能不能访问。不需要Spring Security的@PreAuthorize、@Secured那套复杂的权限模型。
为什么安全做在管道里而不是业务里? 因为靠人遵守规范不如靠机制强制。这是安全工程的第一性原理——不信任人,信任系统。
一句话:安全不是"加一个功能",是"每一层管道都过滤一遍"。当安全成为管道的副产品,而不是额外需求,遗漏就不再是问题。
务实主义的安全观:不追求安全框架的全面性,追求管道设计对安全威胁的天然防御。最好的安全方案,是让不安全的行为"不可能发生",而不是"不允许发生"。
本章小结
• 六道安全防线:绑定变量防注入、SM4注解加密、SM3签名防篡改、commit原子审计、数据层级控制(连接塞机构ID+视图过滤)、操作权限控制(数据库配权限+按钮隐藏)。
• 管道安全的核心收益:业务开发人员不需要"记住"要安全,走管道就自动安全。
• 不买安全框架——六道防线覆盖核心需求,不需要Spring Security/Shiro的复杂配置。
• 最好的安全方案是让不安全的行为"不可能发生",而不是"不允许发生"。
决策洞察:安全不应该靠开发人员"记住"要做,而是走管道就自动安全。能靠架构保证的,不靠人的自觉。