PreparedStatement是唯一可靠的第一道防线,它通过预编译将SQL结构与参数彻底分离,使用户输入(如' OR 1=1 --)仅作为字符串值处理,绝不参与语法解析;必须用?占位并配合setXxx()绑定,严禁字符串拼接或滥用${}。

PreparedStatement 是唯一可靠的第一道防线
别信“加个过滤器就能防注入”这种说法。Java 8 中防止 SQL 注入,PreparedStatement 不是可选项,是强制要求。它把 SQL 结构和参数彻底分离,数据库在预编译阶段就锁死了语句骨架,后续传入的任何内容(哪怕包含 ' OR 1=1 --)都只当字符串值处理,根本不会参与语法解析。
常见错误现象:用 Statement 拼接 "SELECT * FROM user WHERE id = " + userId,即使 userId 是数字类型,攻击者仍可通过类型绕过(如传入 1; DROP TABLE user;)触发堆叠查询。
- 必须用
?占位,再通过pstmt.setString(1, value)、pstmt.setLong(2, id)等方法绑定,不能留空或跳过设置 - 批量操作优先用
addBatch()+executeBatch(),避免循环中反复 prepare - 不要试图在占位符外做“手动转义”,比如先调
StringEscapeUtils.escapeSql()再拼接——这既多余又可能引入新漏洞
MyBatis 里 #{} 和 ${} 的边界必须划死
#{} 在 MyBatis 里等价于 PreparedStatement 参数绑定,安全;${} 是纯文本替换,等同于字符串拼接,高危。很多项目崩溃就倒在把 ${} 用错了地方。
使用场景差异很明确:
立即学习“Java免费学习笔记(深入)”;
-
#{}用于所有值类参数:WHERE name = #{name}、AND status IN (#{status1}, #{status2}) -
${}仅限白名单控制的元数据:表名、列名、ORDER BY字段。例如SELECT * FROM ${tableName},但必须配套校验:if (!List.of("user", "order", "product").contains(tableName)) throw new IllegalArgumentException(); - 绝对禁止
${}出现在WHERE、VALUES或任何能带入用户输入值的位置
异常信息不脱敏 = 主动交出数据库结构
一旦 SQL 执行失败,SQLException.getMessage() 常含敏感细节:MySQL 吐 Unknown column 'pwd_hash' in 'where clause',PostgreSQL 报 relation "users_backup" does not exist——这些全是攻击者探路的坐标。
生产环境必须切断原始消息透出路径:
- HTTP 响应体统一返回
{"code": 500, "message": "操作失败,请稍后重试"},不带任何技术词 - 日志记录用
e.getSQLState()(如23505表示唯一约束冲突)和e.getErrorCode()(驱动特有码),它们稳定、无上下文 - 严禁
e.printStackTrace()写入公开可查的日志文件,尤其不能出现在监控页面或 API 返回体里
输入验证不是补丁,是前置闸机
验证不是为了“让 SQL 更安全”,而是减少无效请求、降低异常触发概率、堵住非参数化路径的漏网之鱼。它和 PreparedStatement 是两层独立防护,不能互相替代。
关键点在于语义化校验,不是关键词过滤:
- ID 类参数直接转类型:
Long.parseLong(idStr),捕获NumberFormatException拒绝,不留给 JDBC 解析 - 邮箱、手机号用正则白名单:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$,不依赖前端 JS 校验 - 分页参数设硬上限:
Math.min(pageSize, 100),防恶意大值拖垮数据库 - 禁用全局“SQL 关键词黑名单”(如拦截
union select drop),规则易绕过且干扰正常业务字段(比如用户名含 “select”)
最危险的疏忽,是以为用了 MyBatis 就万事大吉——只要碰上一个 ${} 或一次手写 Statement,整套防线就形同虚设。


















