2.2 参数装配:井号与美元的分界线


文档摘要

2.2 参数装配:井号与美元的分界线 本节摘要:井号占位走 PreparedStatement 预编译通道,值作为参数设入,天然防注入;美元拼接是运行期文本替换,仅适用于列名、表名、排序字段等无法预编译的位置。本节用一次注入演示划清分界线,给出白名单防护的完整写法,并整理参数命名的全部规则。 手术入口端最重要的一个安全话题。两个长得像兄弟的占位符,走的是两条完全不同的通道。 一条注入演示:两种占位的分野 先看井号占位。下面这条语句在日志里永远显示一个问号: 调用 mapper.findByUsername("zhangsan") 时日志输出: 问号说明值没有进入 SQL 文本。框架先把它交给 PreparedStatement 预编译,再把值作为参数设入。

2.2 参数装配:井号与美元的分界线

本节摘要:井号占位走 PreparedStatement 预编译通道,值作为参数设入,天然防注入;美元拼接是运行期文本替换,仅适用于列名、表名、排序字段等无法预编译的位置。本节用一次注入演示划清分界线,给出白名单防护的完整写法,并整理参数命名的全部规则。

手术入口端最重要的一个安全话题。两个长得像兄弟的占位符,走的是两条完全不同的通道。

一条注入演示:两种占位的分野

先看井号占位。下面这条语句在日志里永远显示一个问号:

<select id="findByUsername" parameterType="string" resultType="User"> SELECT id, username, email FROM users WHERE username = #{username} </select>

调用 mapper.findByUsername("zhangsan") 时日志输出:

==> Preparing: SELECT id, username, email FROM users WHERE username = ? ==> Parameters: zhangsan(String)

问号说明值没有进入 SQL 文本。框架先把它交给 PreparedStatement 预编译,再把值作为参数设入。数据库看到的是一条已经编译好的语句加一个普通数据值——值无论长什么样,都只是值。

换成美元拼接,世界完全不同:

<select id="findByUsernameUnsafe" parameterType="string" resultType="User"> SELECT id, username, email FROM users WHERE username = '${username}' </select>

传入正常用户名时日志是:

==> Preparing: SELECT id, username, email FROM users WHERE username = 'zhangsan' ==> Parameters:

Preparing 行里已经是替换后的完整文本——值在语句编译前就进了 SQL,成为代码的一部分。现在传入恶意输入:

// 恶意输入:注释掉密码条件,或拼一个恒真条件 mapper.findByUsernameUnsafe("anyone' OR '1'='1");

最终执行的 SQL 变成:

SELECT id, username, email FROM users WHERE username = 'anyone' OR '1'='1'

恒真条件命中全表,鉴权形同虚设。同样的输入走井号占位则完全无害——它只是找一个叫"anyone' OR '1'='1"的用户名,查不到就是 null。

02-02-fig01-3

美元拼接的合法场景与白名单防护

预编译的问号只能出现在"值"的位置。列名、表名、排序方向这些语句结构的一部分无法用问号表达,这才是美元拼接的存在理由。

典型需求:列表页的排序字段由前端传入。直接拼接等于把表结构暴露给任意 SQL:

// 危险写法:前端传什么拼什么 List<User> list(String orderBy) // orderBy 可能是 "id" 也可能是恶意文本
<!-- 危险写法的 XML:无校验拼接 --> <select id="findSorted" resultType="User"> SELECT id, username, email FROM users ORDER BY ${orderBy} </select>

防护是白名单:在 Java 侧把允许的取值枚举出来,拒绝一切越界输入。

// 白名单防护:排序字段只认这几个 private static final Set<String> SORTABLE = Set.of("id", "username", "created"); public List<User> findSorted(String field, String direction) { String col = SORTABLE.contains(field) ? field : "id"; // 列名兜底 String dir = "DESC".equalsIgnoreCase(direction) ? "DESC" : "ASC"; // 方向兜底 return mapper.findSorted(col + " " + dir); }
<!-- 拼接点只接收白名单产物 --> <select id="findSorted" resultType="User"> SELECT id, username, email FROM users ORDER BY ${orderBy} </select>

三条纪律:美元拼接的入参必须经过白名单或枚举映射;白名单放在服务层,不依赖前端校验;排序方向不要用正则去"过滤",直接二值映射成 ASC 或 DESC 更简单也更硬。

参数命名的全部规则

参数装配还有一半问题出在"名字"上。规则按参数个数分三种情况。

单参数:名字随意,框架不按名取值(整个参数就是那个值):

<select id="findById" resultType="User"> SELECT id, username FROM users WHERE id = #{anything} <!-- 单参数名字随便写 --> </select>

多参数,不加注解:框架按 arg0、arg1(旧版本是 param1、param2)编号命名,可读性差且容易错位:

List<User> find(String username, String email);
<!-- 不加注解时的两种取值写法,均不推荐 --> <select id="find" resultType="User"> SELECT id, username FROM users WHERE username = #{arg0} AND email = #{param2} </select>

多参数,加注解(推荐):每个参数显式命名,XML 里按名取值:

List<User> find(@Param("username") String username, @Param("email") String email);
<select id="find" resultType="User"> SELECT id, username FROM users WHERE username = #{username} AND email = #{email} </select>

对象参数:直接用属性名,这也是 CRUD 最常见形态:

<insert id="insert" parameterType="User"> INSERT INTO users (username, email) VALUES (#{username}, #{email}) </insert>

一个高频报错在此起源:方法有两个参数、XML 里却写了 #{username},运行时报"Parameter 'username' not found. Available parameters are [arg0, arg1, param1, param2]"。报错信息把可用名字全部列出——看到 arg 开头的名字,就该意识到少了注解。

⚠️ 常见坑:日期参数为 null 时,某些 JDBC 驱动无法从 null 推断类型,报 JdbcType OTHER 错误。写法上补一个类型提示即可:#{created, jdbcType=TIMESTAMP}。这不影响非空值,只为 null 兜底。

💡 关键直觉:评审时见美元就问三句——拼的是结构还是值?入参过白名单了吗?能换成枚举映射吗?三问答不上来就该改。

本节要点回顾

  • 两条通道:井号走预编译数据通道(问号加设值),美元走编译前文本替换通道
  • 注入机理:美元拼接让值变成代码,恒真条件可击穿鉴权;井号下同样的输入只是个查不到的用户名
  • 美元的合法面:列名、表名、排序字段等结构位置,必须配白名单或二值映射
  • 参数命名三态:单参数随意、多参数加注解、对象用属性名;见 arg0 即知漏注解
  • null 日期兜底:jdbcType 提示只为 null 值服务

下一节处理入口端三个进阶场景:一个方法怎么带多个参数对象、插入后怎么拿回自增主键、一批数据怎么高效写进去。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U