直接拼接string构造sql在kotlin spring中极危险,因易导致sql注入;应通过扩展函数强制参数化查询、白名单校验动态片段,并封装审计日志。

为什么直接拼接 String 构造 SQL 在 Kotlin Spring 中很危险
因为 JdbcTemplate 或 NamedParameterJdbcTemplate 本身不阻止字符串拼接,但开发者一不留神就会写出类似 "SELECT * FROM user WHERE name = '" + name + "'" 的代码——这等于把 SQL 注入大门焊死在应用入口。Kotlin 扩展函数不能改变 JDBC 底层行为,但它能用编译期约束和作用域限制,把「容易出错的写法」变成「根本写不出来」。
用扩展函数封装 NamedParameterJdbcTemplate 的安全查询逻辑
核心思路是:把参数名和值的绑定过程收口到扩展函数里,禁止裸字符串参与 SQL 拼接。例如为常见查询模式定义扩展:
fun NamedParameterJdbcTemplate.safeSelectOne(
sql: String,
params: Map<string any>
): Any? = queryForObject(sql, params, object : ParameterizedRowMapper<any> {
override fun mapRow(rs: ResultSet, rowNum: Int): Any = rs.getObject(1)
})</any></string>
使用时只能传预编译 SQL 模板和参数 Map:
val user = jdbcTemplate.safeSelectOne(
"SELECT id, name FROM users WHERE status = :status AND created_at > :since",
mapOf("status" to "ACTIVE", "since" to LocalDateTime.now().minusDays(7))
)
- SQL 字符串必须含命名占位符(
:xxx),不能出现+或StringBuilder拼接 - 参数必须显式声明为
Map<string any></string>,避免传入未清洗的原始请求字段 - 函数签名强制你思考「哪些参数该进 Map」,而不是下意识用
toString()塞进去
对 String 类型做防御性扩展,拦截高危调用
不是所有 SQL 都能提前模板化,比如动态 ORDER BY 或 IN 子句。这时可给 String 加一层校验扩展,只允许白名单内的值参与拼接:
fun String.toSafeOrderBy(): String = when (this) {
"id", "name", "created_at" -> this
else -> throw IllegalArgumentException("Unsafe ORDER BY field: $this")
}
再配合扩展函数组装最终 SQL:
fun buildUserQuery(sortBy: String, ids: List<long>): String {
val safeSort = sortBy.toSafeOrderBy()
val placeholders = ids.joinToString { "?" }
return "SELECT * FROM users WHERE id IN ($placeholders) ORDER BY $safeSort"
}</long>
- 运行时抛异常比 SQL 注入成功后查日志强得多
- 白名单逻辑可抽成配置或枚举,避免硬编码散落在各处
- 注意:这种拼接仅限于无法用参数化的部分(如列名、表名、关键字),数值/字符串值仍必须走
PreparedStatement参数绑定
别忘了 MyBatis 和 JPA 场景下的等效处理
如果项目用的是 MyBatis,扩展函数应聚焦在 SqlSession 调用前的参数校验;若用 JPA,则更适合扩展 CriteriaBuilder 或自定义 @Query 工厂。关键不是换框架,而是让「构造查询」这个动作被一个带契约的函数包裹:
-
@Query注解里的原生 SQL 同样需要静态检查,可用const val定义 SQL 模板,而非字符串字面量 - MyBatis 的
<if></if>标签容易绕过参数绑定,扩展函数可包装SqlSession.selectList,强制要求所有动态片段经toSafeOrderBy()等校验 - 任何扩展都不要试图「自动转义」输入——那是幻觉。真正的防御是控制数据流向,不是修补漏洞
最易被忽略的一点:扩展函数本身不会记录审计日志。如果业务要求追踪谁查了什么,得在扩展内部手动打点,否则你只是把危险操作从 Controller 移到了 Utils 文件里。











