第三部分:安全管道

6 阅读11分钟

第三部分:安全管道

第十一章 政务安全管道——不靠安全框架,靠管道设计

本章核心:六道安全防线全做在框架管道里——绑定变量、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的复杂配置。

• 最好的安全方案是让不安全的行为"不可能发生",而不是"不允许发生"。

决策洞察:安全不应该靠开发人员"记住"要做,而是走管道就自动安全。能靠架构保证的,不靠人的自觉。