文章目录
- JDBC主要组成部分
- 访问数据库步骤
- 数据库交互
- Statement
- PreparedStatement
- SQL注入攻击
- 演示示例
- 单查询
- 多查询
- 返回记录数
JDBC(Java Database Connectivity)是Java中用于执行SQL语句的标准API,它提供了一种统一的方式来访问各种关系型数据库。JDBC使得开发者能够以一种独立于具体数据库的方式编写数据库访问代码。
JDBC主要组成部分
JDBC API:是一组接口和类,定义了与数据库交互所需的方法。
java.sql
包中的核心接口包括Driver
、Connection
、Statement
、PreparedStatement
、CallableStatement
、ResultSet
等。
javax.sql
包则提供了更高级的功能,比如数据源(DataSource)、连接池管理以及分布式事务支持。
JDBC驱动程序:由特定数据库供应商提供的实现,它实现了 JDBC API。
驱动程序负责将 JDBC 调用转换为针对特定数据库系统的适当网络协议或本地库调用。例如 MySQL、Oracle、PostgreSQL 等都有各自的 JDBC 驱动。
JDBC-ODBC桥:是一个旧的机制,允许通过ODBC驱动来访问数据库。由于性能问题和不再维护的原因,在现代应用中已很少使用。
访问数据库步骤
使用JDBC进行数据库操作通常遵循以下步骤:
-
加载驱动:加载适当的JDBC驱动到内存中。
Class.forName("com.mysql.cj.jdbc.Driver");
// 其他数据库驱动 SQL Server : com.microsoft.jdbc.sqlserver.SQLServerDriver MySql : com.mysql.jdbc.Driver Oracle : oracle.jdbc.driver.OracleDriver
-
创建连接:获取到数据库的连接。
Connection conn = DriverManager.getConnection(url, username, password);
-
创建语句:创建一个
Statement
对象来发送SQL命令。Statement stmt = conn.createStatement();
-
执行查询/更新:执行SQL查询或者更新。
ResultSet rs = stmt.executeQuery("SELECT * FROM table_name");
-
处理结果集:遍历
ResultSet
对象以获取查询结果。while (rs.next()) {// 处理每一行数据 }
-
关闭资源:关闭
ResultSet
、Statement
和Connection
以释放资源。rs.close(); stmt.close(); conn.close();
在早期版本的 JDBC 中,通常需要显式地加载 JDBC 驱动程序,例如:
Class.forName("com.mysql.cj.jdbc.Driver");
从 Java 6 开始,JDBC 4.0 引入了服务提供者机制(Service Provider Mechanism),允许自动加载 JDBC 驱动程序。
DriverManager
提供了多种 getConnection()
方法来建立数据库连接:
无参数方法
Connection conn = DriverManager.getConnection(url);
带用户名和密码的方法
Connection conn = DriverManager.getConnection(url, username, password);
带属性的方法
Properties props = new Properties();
props.setProperty("user", username);
props.setProperty("password", password);
// 其他属性...
Connection conn = DriverManager.getConnection(url, props);
使用 DriverManager
来加载驱动程序并建立数据库连接
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;public class DriverManagerExample {public static void main(String[] args) {// 数据库连接信息String url = "jdbc:mysql://localhost:3306/mydatabase";// 防止数据库乱码及日期出错// 放入url连接路径之后 ?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNullString username = "root";String password = "password";try {// 自动加载驱动程序(JDBC 4.0+)// Class.forName("com.mysql.cj.jdbc.Driver"); // 如果需要手动加载// 建立数据库连接Connection conn = DriverManager.getConnection(url, username, password);// 执行数据库操作...// 关闭连接conn.close();} catch (SQLException e) {e.printStackTrace();}}
}
数据库交互
在使用JDBC与数据库进行交互时,PreparedStatement
和Statement
都是用于执行SQL语句的接口
Statement
- 通过
Connection
对象的createStatement()
方法创建 - 适用于执行静态的、不包含参数的SQL语句
- SQL语句直接作为字符串传递,并且可以在字符串中插入变量值(这种方式可能导致SQL注入)
int id = 1001;
String sql = "SELECT * FROM user WHERE id=" + id; // 注意这里的潜在SQL注入风险
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
执行一个SQL查询,根据用户提供的ID查找用户信息
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class PreparedStatementExample {public static void main(String[] args) {String url = "jdbc:mysql://localhost:3306/smbms";String username = "root";String password = "root";try (Connection conn = DriverManager.getConnection(url, username, password)) {// 创建PreparedStatement对象String sql = "SELECT * FROM smbms_user WHERE id = ?"; // 更改表名为正确的名称PreparedStatement pstmt = conn.prepareStatement(sql);int id = 1; // 假设我们需要查询ID为1的用户pstmt.setInt(1, id); // 设置第一个参数的值// 执行查询ResultSet rs = pstmt.executeQuery();// 处理结果集while (rs.next()) {System.out.println("id: " + rs.getInt("id"));System.out.println("userCode: " + rs.getString("userCode"));System.out.println("userName: " + rs.getString("userName"));// 处理其他字段...}// 关闭资源rs.close();pstmt.close();} catch (SQLException e) {e.printStackTrace();}}
}
Statement常用对象
ResultSet executeQuery(sql)
执行sql查询语句,并返回ResultSet对象
int executeUpdate(sql)
执行insert,update,delete语句,返回受影响行数
boolean execute(sql)
执行insert,update,delete语句,返回true或false false成功
executeUpdate()
方法执行更新操作,如插入记录
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "John Doe");
pstmt.setString(2, "john.doe@example.com");
int rowsAffected = pstmt.executeUpdate();
System.out.println(rowsAffected + " row(s) inserted.");
PreparedStatement
- 通过
Connection
对象的prepareStatement(String sql)
方法创建 - 适用于执行带有参数的SQL语句,这些参数用
?
占位符表示 - 使用
?
作为参数占位符,然后通过setXxx()
方法设置参数值(如setInt()
,setString()
等)
String sql = "SELECT * FROM user WHERE id=?";
PreparedStatement ps = conn.prepareStatement(sql);
int id = 1001;
ps.setInt(1, id); // 设置第一个参数的值
ResultSet rs = ps.executeQuery();
区别
PreparedStatement
更适合于需要频繁执行的SQL语句,尤其是那些带有参数的查询。它提供了更好的性能、更高的安全性和更简洁的代码结构。Statement
适用于简单的、一次性执行的SQL语句。
SQL注入攻击
SQL注入攻击是一种常见的网络安全漏洞,它发生在当应用程序使用用户提供的输入来构造SQL查询,并且没有正确地过滤或转义这些输入时。攻击者可以通过在输入中插入恶意的SQL代码来操纵数据库查询,从而执行非授权的数据库操作,如读取敏感数据、修改数据、删除数据等。
// 假设这是用户提供的用户名
String userInput = request.getParameter("username");
// 构造一个查询语句
String query = "SELECT * FROM users WHERE username = '" + userInput + "'";
// 执行查询
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
这段代码非常容易受到SQL注入攻击,因为
userInput
直接被嵌入到了SQL语句中。
SQL 注入类型
1、基于错误的 SQL 注入
// 攻击者的恶意输入
String userInput = "admin' OR '1'='1";
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND (SELECT 1/0) -- '
如果username
字段是唯一的,上面的查询会返回所有记录,因为条件总是为真。如果应用程序抛出了异常并展示了该异常,则可能暴露了关于数据库的信息(通过故意制造一个除以零的错误来获取错误信息)。
2、联合查询注入
String userInput = "admin' UNION SELECT password, null FROM users -- ";
SELECT * FROM users WHERE username='admin' UNION SELECT password, null FROM users -- '
如果username
是唯一的,那么联合查询可能会返回所有用户的密码。
3、布尔盲注
当应用程序不会直接返回任何数据但根据查询结果改变行为时(如页面布局变化),攻击者可以通过发送一系列的请求来猜测数据库中的信息。
String userInput = "admin' AND (SELECT CASE WHEN (1=1) THEN 1 ELSE 0 END) -- ";
SELECT * FROM users WHERE username = 'admin' AND (SELECT CASE WHEN (1=1) THEN 1 ELSE 0 END) -- '
如果查询成功执行并且有结果返回,那么条件(1=1)
为真;反之则为假。
4、时间延迟盲注
当无法从响应内容得知查询结果时,攻击者可以通过使数据库等待一定时间来判断条件是否成立。
String userInput = "admin' AND IF(1=1, SLEEP(5), false) -- ";
SELECT * FROM users WHERE username = 'admin' AND IF(1=1, SLEEP(5), false) -- '
如果条件(1=1)
为真,那么数据库会暂停5秒后再返回结果。
5、 堆叠查询
某些数据库系统允许多条SQL命令一次执行。攻击者可以利用这一点来执行额外的操作。可能不会有任何直接的数据返回给客户端,但可以在数据库上执行额外的操作,比如删除数据或修改记录。(这些操作可能是破坏性的,也可能用于进一步的信息收集)
String userInput = "admin'; DROP TABLE users; --";
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
这个例子中,除了正常的查询外,还附加了一个删除users
表的命令。如果服务器配置允许堆叠查询,那么DROP TABLE users
将会被执行。
防御措施
对于所有这些类型的SQL注入攻击,最佳的防御方法是使用参数化查询或预编译语句(PreparedStatement),并确保对所有用户输入进行严格的验证和清理。此外,应该避免向用户显示详细的错误信息,并限制数据库账户的权限,只提供必要的最小权限。
演示示例
练习使用 JDBC 与 MySQL 数据库进行交互。编写 UserServer
类
三个方法操作 smbms_user
用户表:
findUserById(Long id)
- 该方法根据给定的用户ID查询单个用户的信息。findUserList()
- 该方法查询所有用户,并返回一个包含所有用户的列表。findUserCount()
- 该方法计算并返回用户表中的总记录数。
实体类
public class SmbmsUser {private Integer id; // idprivate String userCode; // 用户编码private String userName; // 用户名称private String userPassword; // 用户密码private Integer gender; // 性别private Date birthday; // 出生日期private String phone; // 电话private String address; // 地址private Integer userRole; // 用户角色private Integer createdBy; // 创建者private Date creationDate; // 创建时间private Integer modifyBy; // 更新者private Date modifyDate; // 更新时间@Overridepublic String toString() {// 重写 toString() 方法...}// get&set 方法...
}
单查询
// 根据 ID 查询用户
public SmbmsUser findUserById(Long id){Connection conn = null;Statement stat = null;try {// 1.加载驱动Class.forName("com.mysql.cj.jdbc.Driver");// 2.创建 Connection 对象(通过 DriverManager)String url = "jdbc:mysql://127.0.0.1:3306/smbms?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull";String user = "root";String pwd = "root";conn = DriverManager.getConnection(url,user,pwd);// 3.创建 Statement 对象(通过 Connection)stat = conn.createStatement();// 4.准备 SQL 语句String sql = "select id,userCode,userName,creationDate from smbms_user where id="+id;// 5.执行 SQL 语句(通过 Statement)ResultSet res = stat.executeQuery(sql);SmbmsUser smbmsUser = null;// 6.处理 resultSet 结果集while(res.next()){smbmsUser = new SmbmsUser();smbmsUser.setId(res.getInt("id"));smbmsUser.setUserCode(res.getString("userCode"));smbmsUser.setUserName(res.getString("userName"));smbmsUser.setCreationDate(res.getDate("creationDate"));}return smbmsUser;} catch (Exception e) {e.printStackTrace();} finally {//7.释放资源try {if(stat != null) stat.close();if(conn != null) conn.close();} catch (SQLException e) {throw new RuntimeException(e);}}return null;
}
多查询
// 查询多用户
public List<SmbmsUser> findUserList(){Connection conn = null;Statement stat = null;try {// 1.加载驱动Class.forName("com.mysql.cj.jdbc.Driver");// 2.创建 Connection 对象(通过 DriverManager)String url = "jdbc:mysql://127.0.0.1:3306/smbms?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull";String user = "root";String pwd = "root";conn = DriverManager.getConnection(url,user,pwd);// 3.创建 Statement 对象(通过 Connection)stat = conn.createStatement();// 4.准备 SQL 语句String sql = "select id,userCode,userName,creationDate from smbms_user";// 5.执行 SQL 语句(通过 Statement)ResultSet res = stat.executeQuery(sql);List<SmbmsUser> list = new ArrayList<>();// 6.处理 resultSet 结果集while(res.next()){SmbmsUser smbmsUser = new SmbmsUser();smbmsUser.setId(res.getInt("id"));smbmsUser.setUserCode(res.getString("userCode"));smbmsUser.setUserName(res.getString("userName"));smbmsUser.setCreationDate(res.getDate("creationDate"));list.add(smbmsUser);}return list;} catch (Exception e) {e.printStackTrace();} finally {//7.释放资源try {if(stat != null) stat.close();if(conn != null) conn.close();} catch (SQLException e) {throw new RuntimeException(e);}}return null;
}
返回记录数
// 返回记录数
public int findUserCount(){Connection conn = null;Statement stat = null;try {// 1.加载驱动Class.forName("com.mysql.cj.jdbc.Driver");// 2.创建 Connection 对象(通过 DriverManager)String url = "jdbc:mysql://127.0.0.1:3306/smbms?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull";String user = "root";String pwd = "root";conn = DriverManager.getConnection(url,user,pwd);// 3.创建 Statement 对象(通过 Connection)stat = conn.createStatement();// 4.准备 SQL 语句String sql = "select count(id) total from smbms_user";// 5.执行 SQL 语句(通过 Statement)ResultSet res = stat.executeQuery(sql);int total = 0;// 6.处理 resultSet 结果集while(res.next()){total = res.getInt("total");}return total;} catch (Exception e) {e.printStackTrace();} finally {//7.释放资源try {if(stat != null) stat.close();if(conn != null) conn.close();} catch (SQLException e) {throw new RuntimeException(e);}}return 0;
}
main 方法
public static void main(String[] args) {UserServer userServer = new UserServer();// 根据 ID 查询用户System.out.println("------- findUserById() 方法获取单个对象 -------");SmbmsUser smbmsUser = userServer.findUserById(2L);System.out.println(smbmsUser.toString());// 查询多用户System.out.println("------- findUserList() 方法获取对象集合 -------");List<SmbmsUser> smbmsUserList = userServer.findUserList();smbmsUserList.forEach(users -> System.out.println(users.toString()));// 返回记录数System.out.println("------- findUserCount() 方法返回记录数 -------");int total = userServer.findUserCount();System.out.println("总记录数为: " + total);
}