严格来说,模板代码是团队里最没有人在 code review 时注意的代码。你新建一个类,IDE 自动带出来的文件头、类声明和注释,你很少会逐字看一遍;同事从旧项目复制一段“大家都这么写”的查询代码,也很少有人去想这段到底有没有 SQL 注入;团队统一用了某份代码格式化模板,可它到底保护了什么,多数人说不清。但恰恰是这些默认行为,日复一日把同样的安全弱点复制进几十上百个文件。我这两年帮三个团队做过“模板代码安全性增强”,结论很一致:这一层投入产出比极高,改动面不大,却能把一批隐性问题直接挡在门外。
这里说的“模板代码”,不只指 IntelliJ IDEA 里的 File and Code Templates,还包括 Live Templates(实时模板)、代码格式化模板,以及团队内部维护的项目骨架代码。它们共同的特点是:自动、批量、低关注。正因如此,安全增强一定要盯住这一层,而不是指望每个开发者在写每一行时都保持警惕。下面这套做法,我按文件模板、实时模板、格式化模板三块拆开讲,最后给落地流程和踩坑经验。
1. 为什么“模板代码”值得单独做一轮安全加固
1.1 模板是漏洞的乘数,不是单点
一个普通的安全漏洞影响一个文件,修复一个文件;但藏在模板里的问题会影响所有从模板生成出来的文件。更麻烦的是,模板代码通常不等于第一次生成的那份文件,它会在迭代里被不断复制、改造成其他形态,最终扩散到搜索都搜不干净的地步。
我见过最典型的案例:某个团队的 REST 控制器文件模板里,写了一段“从请求参数取用户标识并直接拼进 SQL”的示例代码。模板的本意只是告诉新人“查询大概长这样”,结果后面半年里,所有新接口都带着这段代码生长,等于把同一个注入点复制了几十份。等到做安全审计时,修复成本已经是修改模板的几十倍。
这就是模板代码的特有问题:它平时不参与业务逻辑,所以没有人 review;一旦出问题,影响面又是批量级的。做安全性增强,本质上就是把这个“批量扩散”的方向反过来用——模板里放安全默认值,那么每个新文件从出生开始就是相对安全的。
1.2 需要关注的模板不只有一种
我一般会把团队里的模板代码分成三类,每一类的风险点和改进方式都不同。
| 模板类型 | 典型问题 | 安全增强方向 |
|---|---|---|
| 文件模板 | 预置敏感信息、缺失版权声明、生成过于脆弱的结构 | 去掉密钥和内部地址,加强制审查注释,生成防御性默认结构 |
| 实时模板 | 输出 SQL 拼接、明文日志、弱随机数等“复制即用”片段 | 换成参数化 SQL、结构化日志、安全随机数实现 |
| 格式化模板 | 风格混乱导致 diff 过大、可读性差,安全问题被淹没 | 统一换行和缩进,配置 final 默认值,限制 formatter 关闭区 |
很多团队只把“模板代码安全性增强”理解为“改一改类模板”,其实后两类的收益同样明显。实时模板承载的是高频操作,比如日志、判空、查询、随机数;格式化模板承载的是代码审查效率。它们都值得列入加固范围。
1.3 模板代码为什么容易被“低防御心态”对待
还有一个心理层面的原因:IDE 生成的代码会被潜意识当成“可信代码”。我们拿到一个新类时,注意力在业务方法上,而不是默认生成的 logger 声明、类修饰符、注释头;复制一个实时模板片段时,也因为“这是团队模板,应该没问题”而跳过思考。
这恰恰是模板安全增强真正要解决的问题:不是把责任推给个人,而是把默认值调整到正确方向。安全和效率并不冲突,安全默认值反而能省掉后续大量修复时间。
2. IDE 文件模板的改造:让新建代码自带安全默认值
2.1 文件模板在哪里改
在 IntelliJ IDEA 中,路径是Settings → Editor → File and Code Templates。这里能看到的“Files”页签下,有 Class、Interface、Enum、Record、Annotation Type 等内置模板,也可以新增自定义模板。
在编辑模板时,可以直接使用${PACKAGE_NAME}、${NAME}、${USER}、${DATE}这些变量,也可以#parse("File Header.java")把公共头拆到独立模板里。文件的扩展名在“Extension”里指定,比如Class模板默认扩展名是java,模板内容会直接生成到指定文件名和包路径下。
打开模板之前,建议先把“Enable Live Templates”和“Enable File Templates”的默认状态确认一遍,再把不用的模板删掉或禁用。很多团队不是没有安全规范,而是规范分散在十几个模板里,真正用到的只有两三个。
2.2 文件模板的四个加固点
第一,去掉模板里所有预置的敏感信息。有些模板会把内网数据库地址、初始密码、内部服务域名写进去,目的是“省事”。但它们会跟着项目代码一起进入 Git,进入同事的电脑,甚至进入外包人员的本地环境。这类信息一旦泄露,比一个代码漏洞更难收场。模板里只保留占位符或环境变量引用。
第二,统一文件头。使用#parse("File Header.java")引入公共头,公共头里至少包含项目归属、开源协议或版权声明、创建日期。没有版权声明的代码在外发、引入三方依赖时都可能触发法律风险,模板安全增强不能只看漏洞,还要看合规。
第三,生成结构默认偏防御。类尽量是final,logger 使用统一日志框架,公开方法上标注安全审查 TODO。不是每个类都需要final,但作为默认值,它可以阻止无意义的继承扩散。这样生成的代码更短、更可控。
第四,在容易遗漏安全动作的位置强制写 TODO。比如新控制器默认加// TODO: 认证与权限校验,新 Service 方法默认加// TODO: 输入参数校验。开发者在创建文件的第一时间就知道这个文件需要做什么安全动作,而不是等代码上线前才想起来。
2.3 一份可参考的安全类模板
下面是我在项目里常用的一份安全增强后的普通类模板,保留了 IDEA 默认模板的兼容性,也加了一组提醒:
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "") package ${PACKAGE_NAME}; #end #parse("File Header.java") public final class ${NAME} { private ${NAME}() { // 防止无意义实例化 } public void execute() { // TODO: 身份认证与权限校验 // TODO: 输入参数校验 // TODO: 审计日志与敏感数据脱敏 } }这里有两个容易被忽略的点:一是类名${NAME}在 IDEA 内置模板里会自动取新建文件名;二是无参构造函数被私有化,这个默认结构会让开发者思考“我需要暴露实例吗”,而不是随手new。如果团队需要 Spring Bean,可以在模板里直接加@Component并打开构造器注入,但要注意 IDE 文件模板是 Velocity 语法,写${NAME}会正常替换,写$END$这种实时模板变量则不会生效,别混用。
实际使用时,我把execute()方法上的三个 TODO 视为“生成门槛”:接口文档评审时,这三个 TODO 至少要有一个被明确勾掉,否则这个文件不允许进测试环境。这个方法很笨,但很有效。
3. 实时模板:把最容易被复制出洞的代码片段换掉
3.1 实时模板的危险之处
实时模板(Live Templates)是 IDEA 里输入缩写后展开的代码片段,比如输入sout会展开成System.out.println()。它的定位本来就是“高频、短小、省脑子”,所以开发者对它的戒心是最低的。
我见过团队模板库里同时存在两类实时模板:一类是查询模板,展开后直接拼 SQL;一类是日志模板,展开后直接System.out.println用户输入。这两类都是典型的“复制即出洞”模板。SQL 拼接会被注入,明文日志会泄露敏感数据,弱随机数会被预测。问题不在模板本身,而在默认值选错了。
3.2 该替换的模板清单
做一轮实时模板安全增强,最直接的做法是列一个新旧对照表,然后把旧模板删除或改名,迫使肌肉记忆重新建立。
| 场景 | 旧模板 | 安全模板 |
|---|---|---|
| SQL 查询 | stmt.executeQuery("select ... " + input) | PreparedStatement加?占位 |
| 日志输出 | System.out.println(userId + msg) | log.info("userId={}, msg={}", userId, msg) |
| 随机数 | new Random().nextInt() | SecureRandom.getInstanceStrong() |
| 对象判空 | if (obj != null)后自行脑补 | Objects.requireNonNull(obj)显式声明 |
以 SQL 模板为例,我推荐的实时模板长这样,缩写可以设成pv,上下文限定为 Java 语句:
String sql = "SELECT $COLUMNS$ FROM $TABLE$ WHERE $CONDITION$ = ?"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setObject(1, $PARAM$); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { $END$ } } }注意这里用的是=?,不是'$PARAM$'。参数占位符用?的意义不是“看似安全”,而是让 JDBC 驱动把参数作为数据传递,从机制上隔离参数和 SQL 结构。模板里保留$PARAM$作为光标位置,开发者仍需提供实际参数名。这个模板生成后,动态拼接 SQL 的动作就失去了“官方支持”的合法外衣。
日志实时模板也一样。Slf4j 的占位符{}配合参数,让日志内容与格式分离,也方便日志治理平台做脱敏和结构化解析。模板完全禁止用字符串拼接插入用户输入,这是 CWE-117 日志注入的核心防护点。
3.3 如何落地一套安全实时模板
在 IDEA 中,Settings → Editor → Live Templates可以新建分组,比如新建一个名为Security的分组,把上述安全模板放进去。新建时最要注意的是上下文设置:右下角点击“Define”,勾选 Java 的语句或表达式环境。否则模板会在 XML、HTML、SQL 等所有文件里出现,既烦人又容易误触。
变量设置也很关键。点击模板里的Edit variables可以给$PARAM$、$COLUMNS$这类变量设置默认值或下拉候选,$END$是最后一个停留点。如果模板包含${NAME}这种格式,在实时模板里会被识别成变量,需要确认它确实有Edit variables配置,否则展开时 IDEA 会弹提示。
做完替换后,建议在一个临时项目里把模板逐个试一遍。实时模板有一个很隐蔽的问题:它和代码补全设置、导入优化、关键字高亮都会打架,不同版本的 IDEA 对模板变量的支持也有差异。我一般会在模板改完后,让两名资深工程师先实际写两天代码,再全量推广。
4. 代码格式化模板也不只是风格问题:统一是安全审计的前提
4.1 为什么格式化模板也算安全模板
最近我看不少团队在搜 “idea代码格式化模板”,重点都在缩进、换行、括号风格上。但代码格式化模板真正的安全价值,是让代码评审变得高效。一个项目里如果一半人用 4 空格、一半人用 Tab,一半人把方法调用全部缩成一团、一半人习惯链式调用每行断开,那么 diff 里会堆满与业务无关的空白差异。安全审查人员面对海量噪音,就很难定位真正的风险。
所以我会把格式化模板当成安全基础设施来设计。统一格式化之后,每次提交的 diff 只包含真实改动,SQL 注入、硬编码密钥、越权逻辑这类问题才容易被一眼看出来。
4.2 值得在格式化模板里刻意配置的选项
IDEA 的Settings → Editor → Code Style → Java里,有两个入口对安全增强特别有用。
第一个是 Code Generation 标签下的“Make generated local variables final”和“Make generated parameters final”。打开后,IDE 生成代码循环、参数时默认带上final。这看起来只是语法习惯,实际是让不可变思想进入默认代码路径,减少可变共享状态,也就减少并发安全隐患。新代码从出生起就默认“能 final 就 final”。
第二个是 Wrapping and Braces,建议把方法调用链、方法声明、二进制表达式的换行规则调到一个适中的宽度。太宽的代码会让人忽略长链中的异常分支,太窄的代码则会让代码密度过低、上下文很难衔接。我常用 IDEA 默认宽度但刻意把“长方法调用”强制换行,让每一个链式调用的对象和方法都清晰可见。代码评审时,你更容易发现某个位置被换成另一对象。
Formatter Control 这个选项卡也要处理。IDEA 默认情况下支持通过// @formatter:off跳过格式化,这对特殊代码块是便利,但对安全性是漏洞。我建议把“Enable formatter markers in comments”关闭,或者在团队规范里明确:任何 formatter off 区域都必须单独过评审,且要在注释里写明原因。否则你做的格式化模板再严格,也会有人把最危险的一段代码关在门外。
4.3 让格式化模板真正被团队用起来
格式化模板能做到全组统一的关键,不是导出一个 XML 然后让大家手动导入,而是把方案写进项目目录。
在 IDEA 中可以打开 Code Style 的设置齿轮,选择“Store scheme in project”,这个操作会在.idea/codeStyles下生成Project.xml和codeStyleConfig.xml。把这两个文件提交到 Git 仓库,新同事拉下项目后,IDEA 会自动应用项目格式化方案。这比让每个人都去“导入模板”可靠多了。
有一段时期我们认为,格式化模板的分享问题是个“搬文件”问题,后来踩过坑才知道它是“默认值”问题。只要还需要手动导入,就一定会有人漏导入,而漏导入的人产生的代码就会成为下一次大型格式化的导火索。把方案落到仓库里,比培训一万遍都有效。
5. 模板安全增强落地的流程与避坑经验
5.1 从盘点开始,不直接改模板
直接上手改模板是最容易翻车的做法。因为团队往往说不清哪些模板正在使用,哪些模板已经废弃。我推荐的顺序是:
- 盘点模板库。把 IDEA 文件模板、实时模板、格式化方案全部导出,逐个过一遍,标记实际使用频率。
- 制定安全清单。至少包括:不含敏感信息、不含弱随机数、不拼接 SQL、不直接输出用户输入、新类默认 final、公开方法有安全 TODO。
- 改模板并建立试点。先选一个核心小组,用一周时间观察生成代码的编译结果和体验反馈。
- 验证。在试点项目里跑一轮静态分析和安全扫描,看新生成的代码是否覆盖清单中的关键项。
- 同步与文档。把格式化方案放进
.idea/codeStyles,把自定义模板通过设置仓库或模板目录同步,并写一页短文档说明每个模板的意图。 - 定期迭代。每季度重新过一遍模板,因为团队依赖的框架和日志库会变化,模板里的默认值也会过时。
5.2 共享文件模板时容易踩的坑
第一,Velocity 模板里的$符号需要转义。如果你的模板生成 Java 代码里恰好有$这种字符,比如金额模板或者字符串模板,在文件模板中必须写成\$,否则 IDEA 在解析模板时会直接报错。这个坑在新手刚接触模板时几乎必踩。
第二,实时模板和文件模板的变量语法不同。文件模板用${NAME}、#parse,实时模板用$NAME$、$END$。千万不要把两个混合在一起。我在一个团队就见过,把实时模板里的$END$直接放进了文件模板,结果每个新建类都莫名多出一段占位符。
第三,不要一次性格式化整个存量仓库。模板安全增强要作用于新代码,而不是立刻把所有老文件重新格式化一遍。全仓库格式化的巨大 diff 会淹没真实的业务改动,也会让这次安全增强变成一个负面事件。老代码跟着需求迭代逐步迁移就好。
第四,模板的改动也要走 code review。很多团队把模板仓库当成“配置”,谁想改谁就改。但模板是代码的元规则,一次错误修改影响所有生成结果。我会要求模板改动必须有 PR、必须有评审、必须有试点,和核心库改动同等重视。
5.3 我的实际体会
模板代码安全性增强这件事,最有价值的部分不是某个模板怎么写,而是把“生成即安全”这个原则变成团队默认动作。刚开始推进时,反对声会有,主要来自“模板变复杂了”“生成代码变丑了”。但只要坚持一两周,开发者就会适应,反而是在旧项目里手动补安全动作更麻烦。
最后再分享一个经验:模板库要刻意保持克制,不要什么都往里面塞。文件模板里塞满安全依赖、实时模板里塞满工具方法,会让生成代码变得臃肿,最终大家绕开模板手写。安全默认值应该像安全带——存在但不碍事,重要但不用每次都显摆。模板的安全性增强做到“大多数人感受不到变化,但漏洞明显变少”这个状态,就算真正落地了。