一、JDBC 简介
JDBC(Java DataBase Connectivity)就是 Java 数据库连接,说白了就是用 Java 语言来操作数据库。原来我们操作数据库是在控制台使用 SQL 语句来操作数据库,JDBC 是用 Java 语言向数据库发送 SQL 语句。
JDBC(Java Data Base Connectivity,Java 数据库连接)是一种用于执行 SQL 语句的Java API,可以为多种关系数据库提供统一访问,它由一组用 Java 语言编写的类和接口组成
。JDBC 提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。同时,JDBC也是个商标名。
JDBC 屏蔽了底层不同数据库的操作差异,从而使开发者可以通过统一的 Java API 来进行操作不同的数据库,而不必考虑底层具体数据库实现的差异
。
所以尽管底层数据库及其驱动有很多种,但 JDBC 是不变的。
注:JDBC 是一套协议,是 Java 开发人员和数据库厂商达成的协议。
JDBC 只是一组接口,JDBC 的实现是由具体的数据库厂商提供,以驱动程序形式提供,没有驱动无法完成数据库连接
!每个数据库厂商都有自己的驱动,用来连接自己公司的数据库。
当然还有第三方公司专门为某一数据库提供驱动,这样的驱动往往不是开源免费的!
SUN 提供的规范命名为 JDBC,而各个厂商提供的,遵循了 JDBC 规范的,可以访问自己数据库的 API 被称之为驱动!
数据库驱动的实现方式 :
● JDBC-ODBC 桥接式:需要ODBC驱动,适合于企业网或三层结构应用程序(通过将JDBC调用委托给ODBC接口);
● JDBC 网络驱动:将 JDBC 转换为与 DBMS 无关的网络协议,又被某服务器转换为一种 DBMS 协议,以操作各种数据库(驱动程序由中间件服务器提供),这种方式是通过中间服务器的协议转换来实现的;
● JDBC + 本地驱动:需要驱动程序的二进制代码支持(一部分java编写,一部分委托给数据库客户端代码实现),这种方式的安全性比较差;
● JDBC 驱动:将 JDBC 调用直接转换成 JDBC 所使用的网络协议 (全部由 Java 实现,直接和数据库访问),由数据库厂商实现;
JDBC 驱动由数据库厂商提供。在个人开发与测试中,可以使用 JDBC-ODBC 桥连方式;在生产型开发中,推荐使用纯 Java 驱动方式。
二、JDBC 组成
JDBC 有两部分组成:JDBC API 和 JDBC Driver Interface。
1、JDBC API
由 sun 公司提供,内容为供程序员调用的接口与类,集成在 java.sql 和 javax.sql 包中。
JDBC API 可做三件事:与数据库建立连接
、执行 SQL 语句
、处理结果
。
(1)DriverManager 类
JDBC 驱动程序管理器 ,是一工厂实现类,用了工厂方法模式。是JDBC的管理层,作用于用户和驱动程序之间。DriverMangerm可以注册和删除加载的驱动程序,可以根据给定的 url 获取符合 url 协议的驱动 Driver 或者是建立 Conenction 连接,进行数据库交互。有两个作用:
1)注册驱动
:让 JDBC 知道要使用的是哪个驱动所有的驱动程序类必须包含一个静态部分。这个静态部分在加载该实例时由 DriverManager 类进行注册。用户在正常情况下将不会直接调用 DriverManager.regiserDriver 方法,而是在加载驱动程序时由驱动程序自动调用。
注册驱动程序:Class.forName(“com.microsoft.sqlserver.jdbc. SQLServerDriver”); Class.forName(“sun.jdbc.odbc.JdbcOdbcDriver”); Class.forName( "oracle.jdbc.driver.OracleDriver "); // 注册的驱动程序类名称必须在用户的classPath中。
2)
获取 Connection
:它跟踪可用的驱动程序,并在数据库和相应的驱动程序之间建立连接。Connection对象表示连接,与数据库的通讯都是通过这个对象展开的。Connection 最为重要的一个方法就是用来获取 Statement 对象,Statement 是用来向数据库发送 SQL 语句的,这样数据库就会执行发送过来的 SQL 语句。
(2)Dirver 接口
是
驱动程序对象的接口,指向具体数据库驱动程序对象
DriverManager.getDriver(String URL)
在加载某一 Driver 类时,它应该创建自己的实例并向 DriverManager 注册该实例。这意味着用户可以通过调用以下程序加载和注册一个驱动程序:
Class.forName("oracle.jdbc.driver.OracleDriver")
(3)Connection 接口
是连接对象接口,指向具体数据库连接对象
Drivermanager.getConnection(String URL)
Connection 接口会
根据不同的驱动产生不同的连接,负责连接数据库并担任传送数据的任务
。
(4)Statement 接口
执行静态 SQL 语句接口。
Connection.CreateStatement()
由 Connection 产生、负责将要执行的 SQL 语句提交到数据库,这样数据库就会执行发送过来的SQL语句。
(5)ResultSet接口(结果集)
是指向结果集对象的接口
Statement.excuteXXX()
用来接收 select 语句返回的查询结果的,其实质类似于集合。结果集是一个二维的表格,有行有列。
2、JDBC Driver Interface
JDBC API 是提供给开发者的一组独立于数据库的 API,对任何数据库的操作都可以用这组 API 来进行。
那么要把这些通用的 API 翻译成特定数据库能懂的"指令"
,就要由 JDBC Driver Interface 来实现了。JDBC Driver Interface 是面向 JDBC 驱动程序开发商的编程接口,它会把我们通过 JDBC API 发给数据库的通用指令翻译给他们自己的数据库
。
三、JDBC 工作原理
1、装载驱动程序
JDBC 中规定,驱动类在被加载时,需要自己“主动”把自己注册到 DriverManger 中。com.mysql.jdbc.Driver 类的源代码如下:
通过以上源码可知我们注册驱动的时候只是 new 了自己,也就是 Driver。既然这样我们直接把注册驱动类的代码修改为加载驱动类,也可以实现同样的功能:// 利用反射 Class.forName("com.mysql.jdbc.Driver");
2、取得数据库连接
(1)用 DriverManager 或取数据库连接
Connection cn = DriverManager.getConnection(url,uid,pwd);
(2)用 jndi (java的命名和目录服务)方式获取数据库连接,多用于 jsp
DataSource ds = (DataSource) ctx.lookup(jndi); Connection cn = ds.getConnection();
3、执行 sql 语句
获取了连接之后,下面我们就可以获取 Statement,Statement 是用来向数据库发送要执行的 SQL 语句的。Statement最为重要的方法是:
// 执行更新操作,即执行insert、update、delete语句,其实这个方法也可以执行create table、alter table,以及drop table等语句,但我们很少会使用JDBC来执行这些语句; int executeUpdate(String sql); // 执行查询操作,执行查询操作会返回 ResultSet,即结果集。 ResultSet executeQuery(String sql);
(1)用 Statement 来执行 sql 语句(主要是不带参的SQL)
Statement sm = cn.createStatement(); sm.executeQuery(sql); // 执行数据查询语句(select) sm.executeUpdate(sql); // 执行数据更新语句(delete、update、insert、drop等)
(2)用 PreparedStatement 来执行 sql 语句(带参SQL)
String sql = "insert into CUSTOMER(id,name) values (?,?)"; PreparedStatement ps = cn.prepareStatement(sql); ps.setInt(1,xxx); ps.setString(2,xxx); ... ResultSet rs = ps.executeQuery(); // 查询 int c = ps.executeUpdate(); // 更新
(3)用 PreparedStatement 来执行 sql 语句(批量更新或删除SQL)
PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES SET SALARY = ? WHERE ID = ?"); for(int i =0;i<length;i++){ pstmt.setBigDecimal(1, param1[i]); pstmt.setInt(2, param2[i]); pstmt.addBatch(); } pstmt. executeBatch();
(4)用 CallablePrepareStatement 来调用存储过程
CallableStatement cstmt = con.prepareCall("{call getTestData(?, ?)}"); cstmt.registerOutParameter(1, java.sql.Types.TINYINT); cstmt.registerOutParameter(2, java.sql.Types.DECIMAL, 3); cstmt.executeQuery(); byte x = cstmt.getByte(1); java.math.BigDecimal n = cstmt.getBigDecimal(2, 3);
4、事务的处理
JDBC的事务处理简单,在执行多条更新语句后,加cn.commit()或cn.rollback()就可以。
(1)关闭 Connection 的自动提交connection.setAutoCommit(false);
(2)执行一系列sql语句:执行新 sql 前,以前的 Statement(或 PreparedStatemet)须 close
Statement sm ; sm = cn.createStatement(insert into user...); sm.executeUpdate(); sm.close(); sm = cn.createStatement("insert into corp...); sm.executeUpdate(); sm.close();
(3)提交
cn.commit();
(4)如果发生异常,回滚
cn.rollback();
5、处理执行结果
(1)查询语句,返回记录集 ResultSet
(2)更新语句,返回数字,表示该更新影响的记录数(0,表示未更新,-1表示更新失败)
(3)ResultSet 就是一张二维的表格,我们可以调用rs对象的 next() 方法把“行光标”向下移动一行,当第一次调用 next() 方法时,“行光标”就到了第一行记录的位置,这时就可以使用 ResultSet 提供的 getXXX(int col) 方法来获取指定列的数据了。String getString(int columnIndex) // 获取指定列的String类型数据; int getInt(int columnIndex) // 获取指定列的int类型数据; double getDouble(int columnIndex) // 获取指定列的double类型数据; boolean getBoolean(int columnIndex) // 获取指定列的boolean类型数据; Object getObject(int columnIndex) // 获取指定列的Object类型的数据。 String getString(String columnName) // 获取名称为columnName的列的String数据; int getInt(String columnName) // 获取名称为columnName的列的int数据; double getDouble(String columnName) // 获取名称为columnName的列的double数据; boolean getBoolean(String columnName) // 获取名称为columnName的列的boolean数据; Object getObject(String columnName) // 获取名称为columnName的列的Object数据;
上面其实分了两类。一类是根据指定列,一类是根据指定列名。当然如果执行失败了呢?是否要支持滚动呢?
Statement createStatement(int resultSetType, int resultSetConcurrency)
resultSetType 的可选值:
● ResultSet.TYPE_FORWARD_ONLY:不滚动结果集;
● ResultSet.TYPE_SCROLL_INSENSITIVE:滚动结果集,但结果集数据不会再跟随数据库而变化;
● ResultSet.TYPE_SCROLL_SENSITIVE:滚动结果集,但结果集数据不会再跟随数据库而变化;
6、关闭数据库连接
rs.close();
ps.close(); /stat.close();
con.close();
四、sql 注入及解决
1、什么是 sql 注入
SQL 注入(英语:SQL injection),是发生于应用程序与数据库层的安全漏洞。
当我们访问动态网页时, Web 服务器会向数据访问层发起 Sql 查询请求,如果权限验证通过就会执行 Sql 语句。
这种网站内部直接发送的 sql 请求一般不会有危险,但实际情况是
很多时候需要结合用户的输入数据动态构造 Sql 语句,如果用户输入的参数被注入了 Sql 代码,Web 应用又未对动态构造的 Sql 语句使用的用户输入参数进行审查,那么这些注入进去的恶意 sql 就会被数据库服务器误认为是正常的 SQL 指令而运行,因此遭到破坏或是入侵。
2、sql 注入类型
(1)数字类型的注入
当输入参数为数字类型时,例如页码,ID等,存在注入时则为数字类型的注入。 测试方法如下:
http://www.xx.com/a.php?id=1 http://www.xx.com/a.php?id=1’ // 返回异常 http://www.xx.com/a.php?id=1 and 1 =1 // 返回正常 http://www.xx.com/a.php?id=1 and 1 =2 // 返回异常
说明存在注入可能。
这类注入方式常见于PHP和ASP等弱类型语言中,弱类型语言会自动推导数据类型,例,输入的ID的值为1时,会自动推导出ID的数据类型为int型,当输入的ID的值为1 and 1 = 2时,会自动推导出ID的数据类型为string型,但JAVA或者C#这类强类型语言并没有这样的特性,int型强制转换成string型时,会抛出异常,注入失败,所以这类注入方式常见于弱类型语言中。
(2)字串类型的注入
当输入参数为字串类型时,则为字串类型的输入,其与数字类型的注入的区别在于:注入时需要使用单引号来闭合。
测试方法如下:http://host/test.php?name=man' and '1'='1 // 返回成功 http://host/test.php?name=man' and '1'='2 // 返回失败
这里就使上面的数字型变为了字符型。 假设我们网站的SQL语句是这样的:
SELECT * FROM news WHERE name='$name'
当我们构造输入为:man’ and ‘1’='1 语句就变成了:
SELECT * FROM news WHERE name='man' and '1'='1'
可以发现:这个 SQL 已经闭合了。
注意:
字符串注入时需要单引号半闭合,web 后台可以自动将 SQL 闭合
。
(3)搜索型注入点
假设我们的SQL查询语句是这样的:
SELECT * FROM news WHERE keyword like '%$keyword%'
这里的 $keyword 是用户的输入。
当我们输入以下语句:pt%’ and 1=1 and ‘%’=’ 最终我们得到的语句是这样的:
SELECT * FROM news WHERE keyword like '%pt%' and 1=1 and '%'='%'
这个语句又一次的闭合了。
(4)内联式SQL注入
内联注入是指查询注入SQL代码后,原来的查询仍然全部执行。
假设我们的网站SQL查询语句是这样的:SELECT * FROM admin WHER username='$name' AND password ='$passwd'
这一看就是个登录页面的代码。
假如我们将输入语句 ’ or ‘’=’ 提交到登录框中的 username 或者提交到 password 框里面,这两种提交方法是不一样的,我们下面就来分析一下这两个提交方法。
1)提交到 username 我们的语句就会成为这样:// fuzz是我们随便输入的字符串 SELECT * FROM admin WHER username='' or ''='' AND password ='fuzz'
数据库是不会存在username为NULL的字段的,所以第一句返回的是失败,第三句中,因为password是我们随便输入的,99.99%是不会存在这个密码的,于是AND之后,我们的第三句也是失败的,所以整个语句返回失败的。
2)提交到 password 则会是这样的:
// fuzz是我们随便输入的字符串 SELECT * FROM admin WHER username='fuzz' AND password ='' or ''=''
这里 username=‘fuzz’ 是返回失败的,但是’’=’'是返回成功的,OR逻辑是有一个是成功就返回成功,于是我们的整个语句就会返回成功。
返回成功之后我们就会绕过登录表单直接登录系统了
(5)终止式SQL注入
终止式SQL语句注入是指攻击者在注入SQL代码时,通过注释剩下的查询来成功结束该语句,于是被注释的查询不会被执行。我们还是拿(4)那个例子举例:
我们上面已经知道,在 username 框内填入 ’ or ‘’=’ 程序是不会返回成功的,我们就没有办法在 username 做文章了吗? 错了,我们还有终止式。还是上面那个SQL查询语句:
SELECT * FROM admin WHER username='$name' AND password ='$passwd'
这里我们构造如下 username 输入
' or ''='' --
之后我们就可以得到如下的查询语句:
// fuzz是我们随便输入的,--是注释符 SELECT * FROM admin WHER username='' or ''='' --' AND password ='fuzz'
username=’’ 肯定是返回失败的,但是’’=’'会返回成功,而其
后面的 SQL 语句已经被我们注释掉了,是不会执行的
,所以我们还是可以通过在username做这个手脚来绕过登录。下面是我们常见的一些终止方式
终止字符串:
– , #, %23, %00, /*
终止方法:
– , ‘-- , ‘)-- , ) – , ‘)) --, ))–
3、sql 注入示例
(1)初步注入–绕过验证,直接登录
公司网站登陆框如下:
可以看到除了账号密码之外,还有一个公司名的输入框,根据输入框的形式不难推出SQL的写法如下:SELECT * From Table WHERE Name=’XX’ andPassword=’YY’ and Corp=’ZZ’
我发现前两者都做一些检查,而第三个输入框却疏忽了,漏洞就在这里!注入开始,在输入框中输入以下内容:
用户名乱填,密码留空,这种情况下点击登录按钮后竟然成功登录了。我们看一下最终的SQL就会找到原因:SELECT * FromTable WHERE Name=’SQL inject’ and Password=” and Corp=” or 1=1–’
从代码可以看出,前一半单引号被闭合,后一半单引号被 “–”给注释掉,中间多了一个永远成立的条件“1=1”,这就造成任何字符都能成功登录的结果。而Sql注入的危害却不仅仅是匿名登录。
(2)修改数据库信息
网站提供了一个页面,让用户修改自己的昵称、邮件、地址、电话和密码。
更新个人信息的代码是这样写的 如何才能修改自己的 salary 金钱呢?
$conn = getDB(); $sql = "UPDATE credential SET nickname='$nickname',email='$email',address='$address',phonenumber='$phonenumber',Password='$pwd' WHERE id= '$input_id' "; $conn->query($sql))
咱们在命令行查看数据库的时候能够看到 Salary 信息,这样就可以利用漏洞修改Salary 。
漏洞:$sql = "UPDATE credential SET nickname='$nickname',...
用户输入的攻击串:
',salary='9999999' where Name='Alice'#
这样数据库就错误的把用户输入的恶意 sql 误认为是正常的 SQL 指令而运行。
4、sql 注入的预防
1、检查变量数据类型和格式
始终通过测试类型、长度、格式和范围来验证用户输入。
如果你的SQL语句是类似where id={$id} 这种形式,数据库里所有的id都是数字,那么就应该在SQL被执行前,检查确保变量id是int类型;如果是接受邮箱,那就应该检查并严格确保变量一定是邮箱的格式,其他的类型比如日期、时间等也是一个道理。总结起来:只要是有固定格式的变量,在SQL语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL注入攻击。
2、测试输入的大小和数据类型,强制执行适当的限制
测试字符串变量的内容,只接受所需的值。拒绝包含二进制数据、转义序列和注释字符的输入内容。这有助于防止脚本注入,防止某些缓冲区溢出攻击。
使用 XML 文档时,根据数据的架构对输入的所有数据进行验证。
绝不直接使用用户输入内容来生成 Transact-SQL 语句。
3、PreparedStatement:绑定变量,使用预编译语句
MySQL的mysqli驱动提供了预编译语句的支持,不同的程序语言,都分别有使用预编译语句的方法
实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式,使用预编译的SQL语句语义不会发生改变,在SQL语句中,变量用问号?表示,这样就无法改变SQL语句的结构。
sql 注入只对 sql 语句的编译过程有破坏作用,而 PreparedStatement 在接收用户输入前已经编译好了,执行阶段只是把输入串作为数据处理,而不再对 sql 语句进行解析编译,因此也就避免了 sql 注入问题
。
PrepareStatement 可以防止sql注入是采用了预编译的方法,先将SQL语句中可被客户端控制的参数集进行编译,生成对应的临时变量集,再使用对应的设置方法,为临时变量集里面的元素进行赋值,赋值函数setString(),会对传入的参数进行强制类型检查和安全检查,所以就避免了SQL注入的产生。
参数化能防注入的原因在于:语句是语句,参数是参数,参数的值并不会成为语句的一部分,数据库只按语句的语义执行而不受参数影响
。