防止SQL注入的核心就是一条铁律:永远不要把用户输入直接拼接到SQL语句里。不管你用MyBatis还是Hibernate,只要你用了字符串拼接的方式构造查询条件,就等于给攻击者敞开了大门。具体到操作层面,MyBatis要用#{}参数绑定而不是${}字符串替换,Hibernate要用HQL/Criteria API的参数化查询而不是拼接HQL字符串。下面我把两个框架各自的注意事项、容易踩的坑、最佳实践全部列出来,照着做基本不会出问题。

一、MyBatis防止SQL注入的核心注意事项

MyBatis提供了两种参数占位符:#{}和${}。#{}是预编译参数绑定,底层用PreparedStatement,会对参数做转义处理,天然防注入。${}是直接字符串替换,等于把参数原样塞进SQL里,极其危险。很多人在写动态SQL时图方便用${},这是最常见的注入漏洞来源。

正确写法示例:

<select id="findUserByName" resultType="User">
    SELECT * FROM user WHERE name = #{name}
</select>

错误写法示例:

<select id="findUserByName" resultType="User">
    SELECT * FROM user WHERE name = '${name}'
</select>

上面错误写法中,如果用户输入的name是"admin' OR '1'='1",整个SQL就变成了SELECT * FROM user WHERE name = 'admin' OR '1'='1',直接绕过认证。所以第一条铁律:任何地方都不要用${}来接收用户输入的值。

二、MyBatis中${}的安全使用场景

说${}完全不能用也不客观。在某些场景下${}是合理的,比如动态指定表名、列名、排序字段。但前提是这些值必须来自程序内部的枚举、白名单或者经过严格校验的固定值,绝对不能来自前端用户输入。

<select id="findUser" resultType="User">
    SELECT * FROM user
    ORDER BY ${columnName} ${order}
</select>

上面这个例子,columnName和order如果是前端传过来的,就有注入风险。正确做法是在Java代码里做白名单校验:

public List<User> findUser(String columnName, String order) {
    // 白名单校验
    if (!Arrays.asList("name", "age", "create_time").contains(columnName)) {
        throw new IllegalArgumentException("非法排序字段");
    }
    if (!"ASC".equalsIgnoreCase(order) && !"DESC".equalsIgnoreCase(order)) {
        throw new IllegalArgumentException("非法排序方式");
    }
    return userMapper.findUser(columnName, order);
}

三、MyBatis动态SQL中的拼接陷阱

用<if>、<where>、<foreach>等标签写动态SQL时,很多人会不自觉地用${}来拼接条件。比如下面这种写法:

<select id="searchUser" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null">
            AND name = '${name}'
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
</select>

这里name用了${},status用了#{},混着来特别容易出错。建议所有用户输入的条件统一用#{},养成习惯。另外<foreach>遍历集合时,item属性的值也要用#{}:

<foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
</foreach>

四、MyBatis的TypeHandler也要注意

自定义TypeHandler时,如果在handler内部直接拼接SQL字符串,同样会引入注入风险。正确做法是在TypeHandler的setParameter方法中,通过PreparedStatement的set方法来设置参数,而不是自己拼SQL。

public class SafeTypeHandler implements TypeHandler<String> {
    @Override
    public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) 
            throws SQLException {
        ps.setString(i, parameter);  // 安全
    }
}

五、Hibernate防止SQL注入的核心注意事项

Hibernate的防注入思路和MyBatis类似,核心就是参数化查询。Hibernate提供了HQL、Criteria API、CriteriaBuilder、原生SQL等多种查询方式,其中HQL和Criteria API天然支持参数绑定,而原生SQL如果用字符串拼接就会有风险。

HQL参数化查询的正确写法:

String hql = "FROM User WHERE name = :name";
Query query = session.createQuery(hql);
query.setParameter("name", userInput);

如果你用位置参数:

String hql = "FROM User WHERE name = ?1";
Query query = session.createQuery(hql);
query.setParameter(1, userInput);

这两种写法都是安全的,Hibernate底层会用PreparedStatement来处理。

六、Hibernate中容易踩坑的原生SQL

当你需要用原生SQL时,必须用参数绑定,不能拼接字符串。错误写法:

String sql = "SELECT * FROM user WHERE name = '" + userInput + "'";
SQLQuery query = session.createSQLQuery(sql);

正确写法:

String sql = "SELECT * FROM user WHERE name = :name";
SQLQuery query = session.createSQLQuery(sql);
query.setParameter("name", userInput);

或者用位置参数:

String sql = "SELECT * FROM user WHERE name = ?";
SQLQuery query = session.createSQLQuery(sql);
query.setParameter(0, userInput);

七、Hibernate Criteria API的安全优势

Criteria API和CriteriaBuilder是完全面向对象的查询方式,不需要写任何SQL字符串,从根本上杜绝了拼接注入的可能。推荐在复杂动态查询场景下优先使用。

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.select(root).where(cb.equal(root.get("name"), userInput));
List<User> results = entityManager.createQuery(cq).getResultList();

这种写法完全不涉及字符串拼接,是最安全的方式之一。唯一的缺点是代码量稍多,但安全性和可维护性都更好。

八、Hibernate的@NamedQuery和@Query注解

在JPA注解中定义查询时,同样要用参数绑定。正确写法:

@NamedQuery(
    name = "User.findByName",
    query = "SELECT u FROM User u WHERE u.name = :name"
)

然后在代码中调用:

TypedQuery<User> query = entityManager.createNamedQuery("User.findByName", User.class);
query.setParameter("name", userInput);

千万不要在注解里用字符串拼接,比如query = "SELECT u FROM User u WHERE u.name = '" + something + "'",这在运行时是不合法的,但有些人会在动态构建查询字符串时犯类似错误。

九、两个框架通用的防护建议

第一,开启数据库层面的权限最小化。应用程序连接数据库的账号只给必要的SELECT、INSERT、UPDATE、DELETE权限,不要给DROP、ALTER等高权限,即使被注入也能限制破坏范围。

第二,使用WAF或者应用层的输入过滤。对所有用户输入做类型校验、长度限制、特殊字符过滤。比如用户名只允许字母数字下划线,年龄只允许正整数。

第三,开启框架的SQL日志但不要在生产环境输出完整SQL。通过日志可以排查是否存在拼接行为,但生产环境要避免泄露SQL结构给攻击者。

第四,定期做代码审计和安全扫描。用SonarQube、FindBugs等工具扫描项目,重点关注SQL拼接相关的告警。很多团队的注入漏洞都是历史代码遗留的,新代码反而比较规范。

第五,框架版本要及时更新。MyBatis和Hibernate的新版本会修复已知的安全问题和底层漏洞,保持框架在稳定的较新版本上运行。

十、总结与核心清单

把上面的内容浓缩成一份清单,方便你对照检查:

1. MyBatis所有用户输入参数用#{},绝不用${}接收前端数据。

2. MyBatis中${}只用于程序内部白名单控制的表名、列名、排序方向。

3. MyBatis动态SQL的<foreach>、<if>标签内统一用#{}。

4. MyBatis自定义TypeHandler用PreparedStatement.setXxx方法传参。

5. Hibernate用HQL或Criteria API时,所有参数用setParameter绑定。

6. Hibernate原生SQL必须用命名参数或位置参数,禁止字符串拼接。

7. 复杂动态查询优先用Hibernate CriteriaBuilder,零拼接风险。

8. 数据库连接账号遵循最小权限原则。

9. 应用层做输入校验和类型过滤。

10. 定期代码审计,保持框架版本更新。

SQL注入是OWASP Top 10里年年上榜的老问题,但每年依然有大量系统中招。根本原因不是技术不知道,而是开发时图省事、赶进度、老代码没人管。把上面这份清单落实到团队的Code Review规范里,基本就能把这个风险降到最低。