第43天-JavaEE与ORM框架安全指南:从JDBC到Hibernate/MyBatis的SQL注入防御

admin 2026-03-03 08:03:58 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细阐述了JavaEE开发中从JDBC到Hibernate及MyBatis框架防御SQL注入的策略。核心结论是必须使用预编译机制实现代码与数据分离,具体建议包括JDBC使用PreparedStatement、Hibernate使用命名参数绑定、MyBatis优先使用#{}占位符。文章强调避免字符串拼接,遵循最小权限原则,并结合输入校验与代码审计构建纵深防御体系,确保数据访问层安全。 综合评分: 85 文章分类: 安全开发,WEB安全,漏洞分析,代码审计


cover_image

第43天-JavaEE与ORM框架安全指南:从JDBC到Hibernate/MyBatis的SQL注入防御

原创

萧瑶 萧瑶

AlphaNet

2026年2月21日 15:31 韩国

掌握数据访问层的正确姿势,让SQL注入漏洞无处遁形

前言

在JavaEE企业级开发中,数据持久化是核心环节。从最原始的JDBC操作,到Hibernate、MyBatis等ORM框架的普及,开发效率大幅提升,但SQL注入这一经典安全威胁始终如影随形。本文将带你梳理JavaEE中Servlet、过滤器、监听器的基础概念,重点剖析JDBC、Hibernate、MyBatis三种数据访问技术的正确使用姿势,并通过安全与不安全代码的对比,彻底理解预编译机制如何防御SQL注入。

一、重温JavaEE基础:Servlet、过滤器与监听器

在正式进入数据层之前,我们先简单回顾JavaEE三大组件:

· Servlet:处理客户端请求并生成动态响应的Java程序。生命周期由容器管理(init→service→destroy)。

· 过滤器(Filter):在请求到达Servlet之前或响应离开Servlet之后执行预处理和后处理,常用于编码设置、权限校验、日志记录。

· 监听器(Listener):监听Web应用中的事件(如Session创建、属性变化),触发相应的操作。

这些组件构成了JavaEE Web应用的基础骨架,后续的ORM操作通常封装在Servlet或业务层中调用。

二、Maven项目配置

所有示例均基于Maven构建,只需在pom.xml中引入对应依赖即可。Maven核心配置可参考这篇文章。依赖的坐标统一从Maven中央仓库获取。

三、JDBC原生操作与SQL注入

JDBC(Java Database Connectivity)是Java访问数据库的基石。我们通过一个完整的查询示例来看。

  1. 标准JDBC步骤
// 1. 引用依赖(mysql-connector-java)

// 2. 注册驱动(可选,高版本可自动注册)

Class.forName("com.mysql.cj.jdbc.Driver");

// 3. 建立连接

String url = "jdbc:mysql://localhost:3306/phpstudy?serverTimezone=UTC";

Connection conn = DriverManager.getConnection(url, "root", "123456");

// 4. 创建Statement并执行SQL

Statement stmt = conn.createStatement();

String sql = "SELECT \* FROM admin WHERE id = " + id;  // 拼接参数

ResultSet rs = stmt.executeQuery(sql);

// 5. 处理结果集

while (rs.next()) {

    int id = rs.getInt("id");

    String title = rs.getString("page\_title");

    // ...

}

// 6. 关闭资源
  1. 注入漏洞演示

上述代码中,SQL语句直接拼接用户输入的id,攻击者传入1 or 1=1即可绕过条件,查询全部数据。更危险的是,通过’; DROP TABLE admin; –可删除表。

  1. 预编译防御:PreparedStatement

安全的写法必须使用预编译(PreparedStatement):

String sql = "SELECT \* FROM admin WHERE id = ?";

PreparedStatement pstmt = conn.prepareStatement(sql);

pstmt.setInt(1, Integer.parseInt(id));  // 参数化设置

ResultSet rs = pstmt.executeQuery();

原理:预编译会将SQL语句的结构(骨架)与参数分开,数据库先编译SQL骨架,再将参数作为纯数据传入,无法改变SQL语义,从而彻底杜绝注入。

四、Hibernate框架与HQL注入

Hibernate作为经典的ORM框架,通过对象-关系映射简化了数据库操作。但HQL(Hibernate Query Language)如果使用不当,同样存在注入风险。

  1. 配置文件 hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>

<!DOCTYPE hibernate-configuration PUBLIC

&nbsp; &nbsp; &nbsp; &nbsp; "-//Hibernate/Hibernate Configuration DTD 3.0//EN"

&nbsp; &nbsp; &nbsp; &nbsp; "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

&nbsp; &nbsp; <session-factory>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 数据库连接配置 -->

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.connection.driver\_class">com.mysql.cj.jdbc.Driver</property>

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/phpstudy?serverTimezone=UTC</property>

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.connection.username">root</property>

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.connection.password">123456</property>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 方言 -->

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</property>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 显示SQL -->

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.show\_sql">true</property>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 自动更新表结构 -->

&nbsp; &nbsp; &nbsp; &nbsp; <property name="hibernate.hbm2ddl.auto">update</property>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 映射实体类 -->

&nbsp; &nbsp; &nbsp; &nbsp; <mapping class="com.example.entity.User"/>

&nbsp; &nbsp; </session-factory>

</hibernate-configuration>
  1. 实体类 User.java
package com.example.entity;

import javax.persistence.\*;

@Entity

@Table(name = "admin")

public class User {

&nbsp; &nbsp; @Id

&nbsp; &nbsp; @GeneratedValue(strategy = GenerationType.IDENTITY)

&nbsp; &nbsp; private Integer id;

&nbsp; &nbsp; private String username;

&nbsp; &nbsp; private String password;

&nbsp; &nbsp; // getter/setter 省略

}
  1. 工具类 HibernateUtil.java
package com.example.util;

import org.hibernate.SessionFactory;

import org.hibernate.cfg.Configuration;

public class HibernateUtil {

&nbsp; &nbsp; private static final SessionFactory sessionFactory = buildSessionFactory();

&nbsp; &nbsp; private static SessionFactory buildSessionFactory() {

&nbsp; &nbsp; &nbsp; &nbsp; try {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return new Configuration().configure().buildSessionFactory();

&nbsp; &nbsp; &nbsp; &nbsp; } catch (Throwable ex) {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw new ExceptionInInitializerError(ex);

&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; }

&nbsp; &nbsp; public static SessionFactory getSessionFactory() {

&nbsp; &nbsp; &nbsp; &nbsp; return sessionFactory;

&nbsp; &nbsp; }

}
  1. Servlet查询示例
package com.example.servlet;

import com.example.entity.User;

import com.example.util.HibernateUtil;

import org.hibernate.Session;

import org.hibernate.query.Query;

import javax.servlet.\*;

import javax.servlet.http.\*;

import java.io.IOException;

import java.util.List;

public class UserQueryServlet extends HttpServlet {

&nbsp; &nbsp; protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

&nbsp; &nbsp; &nbsp; &nbsp; String username = req.getParameter("username");

&nbsp; &nbsp; &nbsp; &nbsp; // 不安全的HQL拼接

&nbsp; &nbsp; &nbsp; &nbsp; // String hql = "FROM User WHERE username = '" + username + "'";

&nbsp; &nbsp; &nbsp; &nbsp; // 安全的命名参数方式

&nbsp; &nbsp; &nbsp; &nbsp; String hql = "FROM User WHERE username = :username";

&nbsp; &nbsp; &nbsp; &nbsp; try (Session session = HibernateUtil.getSessionFactory().openSession()) {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Query<User> query = session.createQuery(hql, User.class);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; query.setParameter("username", username); &nbsp;// 绑定参数

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; List<User> list = query.list();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 处理结果...

&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; }

}
  1. 注入风险与防御

· 不安全写法:直接拼接用户输入到HQL,如”FROM User WHERE username='”+username+”‘”。攻击者输入’ or ‘1’=’1可改变查询逻辑。

· 安全写法:使用命名参数(如:username)或位置参数(?),通过setParameter绑定。Hibernate底层会将参数化的HQL转换为预编译的SQL,参数值被安全处理。

五、MyBatis框架与#{} vs ${}

MyBatis是一款半自动ORM框架,通过SQL映射文件灵活管理SQL。它的注入风险主要来自参数占位符的选择。

  1. 配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration

&nbsp; &nbsp; &nbsp; &nbsp; PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

&nbsp; &nbsp; &nbsp; &nbsp; "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

&nbsp; &nbsp; <environments default="development">

&nbsp; &nbsp; &nbsp; &nbsp; <environment id="development">

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <transactionManager type="JDBC"/>

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <dataSource type="POOLED">

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <property name="driver" value="com.mysql.cj.jdbc.Driver"/>

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <property name="url" value="jdbc:mysql://localhost:3306/phpstudy?serverTimezone=UTC"/>

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <property name="username" value="root"/>

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <property name="password" value="123456"/>

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dataSource>

&nbsp; &nbsp; &nbsp; &nbsp; </environment>

&nbsp; &nbsp; </environments>

&nbsp; &nbsp; <mappers>

&nbsp; &nbsp; &nbsp; &nbsp; <mapper resource="AdminMapper.xml"/>

&nbsp; &nbsp; </mappers>

</configuration>
  1. Mapper文件 AdminMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper

&nbsp; &nbsp; &nbsp; &nbsp; PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

&nbsp; &nbsp; &nbsp; &nbsp; "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.AdminMapper">

&nbsp; &nbsp; <!-- 安全写法:使用#{}预编译 -->

&nbsp; &nbsp; <select id="selectAdminById" resultType="com.example.model.Admin">

&nbsp; &nbsp; &nbsp; &nbsp; SELECT \* FROM admin WHERE id = #{id}

&nbsp; &nbsp; </select>

&nbsp; &nbsp; <!-- 不安全写法:使用${}字符串替换 -->

&nbsp; &nbsp; <select id="selectAdminByUsernameUnsafe" resultType="com.example.model.Admin">

&nbsp; &nbsp; &nbsp; &nbsp; SELECT \* FROM admin WHERE username = '${username}'

&nbsp; &nbsp; </select>

</mapper>
  1. 实体类 Admin.java 与 Mapper接口
package com.example.model;

public class Admin {

&nbsp; &nbsp; private Integer id;

&nbsp; &nbsp; private String username;

&nbsp; &nbsp; private String password;

&nbsp; &nbsp; // getter/setter

}

package com.example.mapper;

import com.example.model.Admin;

public interface AdminMapper {

&nbsp; &nbsp; Admin selectAdminById(Integer id);

&nbsp; &nbsp; Admin selectAdminByUsernameUnsafe(String username); // 演示用,切勿使用

}
  1. Servlet调用示例
package com.example.servlet;

import com.example.mapper.AdminMapper;

import com.example.model.Admin;

import org.apache.ibatis.io.Resources;

import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import javax.servlet.http.\*;

import java.io.InputStream;

public class SelectServlet extends HttpServlet {

&nbsp; &nbsp; protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

&nbsp; &nbsp; &nbsp; &nbsp; String id = req.getParameter("id");

&nbsp; &nbsp; &nbsp; &nbsp; try {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; String resource = "mybatis-config.xml";

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; InputStream inputStream = Resources.getResourceAsStream(resource);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try (SqlSession session = sqlSessionFactory.openSession()) {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; AdminMapper mapper = session.getMapper(AdminMapper.class);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 使用安全的#{}查询

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Admin admin = mapper.selectAdminById(Integer.parseInt(id));

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 输出结果...

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; } catch (Exception e) {

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e.printStackTrace();

&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; }

}
  1. #{} 与 ${} 的本质区别

· #{}:MyBatis会将其解析为预编译语句(PreparedStatement)的参数占位符?,并安全设置参数值,可防止SQL注入。

· ${}:直接进行字符串替换,将参数值拼接到SQL语句中,存在SQL注入风险。仅在极少数情况(如表名、列名动态变化)下使用,且必须对输入做严格校验。

六、Spring JPA 简述

Spring Data JPA 是对Hibernate的更高层封装,默认使用预编译和命名参数,其查询方法(如findByUsername(String username))自动生成安全的SQL。但若使用@Query注解且手动拼接字符串,同样会有注入风险:

// 安全

@Query("SELECT u FROM User u WHERE u.username = :username")

User findByUsername(@Param("username") String username);

// 不安全(禁止!)

@Query("SELECT u FROM User u WHERE u.username = '" + username + "'")

七、总结与最佳实践

技术 安全写法 危险写法 核心防御机制

JDBC PreparedStatement + ? Statement + 字符串拼接 预编译 + 参数化查询

Hibernate 命名参数 :name HQL字符串拼接 参数绑定

MyBatis #{xxx} ${xxx} 预编译占位符

Spring JPA 命名参数或方法命名查询 @Query中拼接 底层Hibernate参数绑定

安全开发黄金法则:

  1. 首选预编译:任何与数据库交互的SQL/HQL语句,都必须使用参数化查询。

  2. 避免拼接:绝对不要将外部输入直接拼接到SQL语句中。

  3. 最小权限原则:数据库连接使用专用账号,限制必要权限。

  4. 输入校验:在业务层对参数做类型、格式、范围的校验,作为纵深防御。

  5. 定期代码审计:使用静态分析工具(如FindBugs、SonarQube)扫描SQL注入漏洞。

从JDBC到Hibernate再到MyBatis,虽然技术不断演进,但抵御SQL注入的核心思想始终未变:将代码与数据分离。希望本文能帮助你在日常开发中写出既高效又安全的数据库访问代码。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:AlphaNet 萧瑶 萧瑶《第43天-JavaEE与ORM框架安全指南:从JDBC到Hibernate/MyBatis的SQL注入防御》

评论:0   参与:  0