能——sqlx::query!宏在编译期强制SQL为字面量、参数仅通过$1/?占位符绑定,禁止字符串拼接,从而从根源阻断SQL注入路径;它校验语法、表字段、类型匹配,错误直接导致编译失败。

SQLx 的 query! 宏真能在编译期拦住 SQL 注入?
不能——但能拦住绝大多数因拼接字符串导致的语法错误和类型错位,而这类错误恰恰是 SQL 注入的温床。Rust 1.75 中 sqlx::query!(注意带感叹号)要求 SQL 字符串必须是字面量(literal),且在编译期解析语句结构、绑定参数位置、推导返回列类型。这意味着你无法把用户输入塞进 SQL 字符串里再交给它处理,客观上堵死了字符串拼接式注入路径。
关键点在于:query! 不接受 &str 变量、不接受 format! 结果、不接受任何运行时构造的 SQL 片段。一旦尝试传入非字面量,编译器直接报错:macro requires a string literal。
为什么 query! 比 query 更安全?
query(无感叹号)是运行时函数,接收 &str,完全信任你传入的内容;query! 是过程宏,在编译期做三件事:校验 SQL 语法、匹配数据库方言(如 PostgreSQL vs SQLite)、生成类型安全的执行器。它强制所有参数通过 $1, $2(PostgreSQL)或 ?(SQLite/MySQL)占位符传入,且这些占位符位置和数量必须与后续 .bind() 调用严格一致。
常见错误现象:
- 写成
query!("SELECT * FROM users WHERE name = '" + name + "'")→ 编译失败:expected string literal - 漏掉
.bind()或多 bind 一个值 → 编译失败:too many/few arguments for query - 对整数字段 bind 了
String→ 编译失败:expected i32, found String
这些都不是运行时 panic,是铁定过不了编译的硬性约束。
实际怎么写才真正防御注入?
核心原则:用户输入只出现在 .bind() 链中,绝不参与 SQL 字符串拼接。哪怕只是动态表名或列名,也不能用 query! —— 它不支持,你得换方案。
正确做法示例(PostgreSQL):
let email = "alice@example.com";
let user = sqlx::query!("SELECT id, name FROM users WHERE email = $1", email)
.fetch_one(&pool)
.await?;容易踩的坑:
-
query!不支持动态表名/列名:想按条件查不同表?不能写query!("SELECT * FROM {}", table_name),得用白名单 + match 分支,或降级用query_as_unchecked!(不推荐) - 参数顺序必须和 SQL 中占位符严格对应:第 n 个
$n对应第 n 个.bind(),错位会编译失败 - PostgreSQL 使用
$1,$2;SQLite/MySQL 用?,别混用;宏会根据default_features = false和启用的数据库 feature 自动适配 - 如果查询结果列类型不确定(比如聚合函数或表达式),用
query_as!显式指定 struct,否则编译器可能推导失败
编译期防御的边界在哪?
它防不住逻辑层注入,比如用户输入 "admin' -- " 本意是绕过密码检查,但如果你在应用层用它去查 username = $1,query! 照样放行——因为这是合法的字符串值,不是非法 SQL。真正的防护靠的是:参数化查询(query! 提供了强保障)、输入清洗(如邮箱格式校验)、权限隔离(如只查 own_id 字段)、最小权限 DB 用户。
最容易被忽略的一点:你启用了 sqlx::postgres feature,但忘了在 Cargo.toml 里加 features = ["postgres"],会导致 query! 无法识别 $1 占位符,报错信息晦涩(如 unexpected token "$")。务必确认 feature 和数据库驱动匹配。

















