浅析JDBC

JDBC浅析

connection

connection是数据库连接对象,我们针对于数据库的一系列操作都以来connection这个对象完成。

简单来说,我们利用java代码连接数据库的时候,初始需要两个步骤。

  1. 注册驱动
  2. 获取连接

注册驱动

首先,我们要先了解,java通过java.sql.DriverManager来管理所有数据库驱动的,注册驱动的目的是为了连接数据库。

注册驱动有三种方式:

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

    此方式由于参数为字符串,因此很容易修改,移植性强。因此这也是最常用的注册方式,也是推荐的方式。

    好处:能够在编译时不依赖于特定的jdbc Driver库,也就是减少了项目代码的依赖性,而且也很容易改造成从配置文件读取JDBC配置,从而可以在运行时动态更换数据库连接驱动。

  2. System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");

    这种方式的好处在于可以同时导入多个jdbc驱动,在多个驱动之间,需要使用冒号“:”分成。

    例如:System.setProperty("jdbc.drivers","XXXDriver,XXXDriver,XXXDriver");

    这样就一次注册了三个数据库驱动。

  3. new com.mysql.jdbc.Driver();

    这里不需要这样写DriverManager.registerDriver(new com.mysql.jdbc.Driver()),原因是com.mysql.jdbc.Driver类的静态代码块里面已经进行了修改的操作。我们通过Driver类的源码可以了解到,Driver类中就有一个静态的代码块,只要我们执行了Driver类中的静态代码块,并把驱动的实力放入到了Driver的一个数组列表中,我们再调用方法registerDrever就相当于又向drivers列表中放了一次dirver驱动。

    static {
    	try{
    		java.sql.DriverManager.registerDriver(new Driver());
    	} catch(SQLException E){
    		throw new RuntimeException("Can't register driver!");
    	}
    }
    

    由new com.mysql.jdbc.Driver()可以知道,这里需要创建一个类的实例。创建类的事例就需要在java文件中将该类通过import导入,否则就会报错,即采用这种方式,程序在编译的时候不能脱离驱动类包,为程序切换到其他数据库带来麻烦。

    jdbc是使用桥的模式进行连接的。DriverManager就是管理数据库驱动的一个类,java.sql.Driver就是一个提供注册数据库驱动的接口,而com.microsoft.sqlserver.jdbc.SQLServerDriver()是java.sql.Driver接口的一个具体实现。

获取连接

获取连接的过程,就是在注册了驱动之后,我们把可以数据库的地址和账户密码写到代码里面,完成连接的过程。

详细代码:

Class.forName("com.mysql.jdbc.Driver");
//获取连接对象
conn = DriverManager.getConnection("jdbc:mysql:///db1","root1","password");

数据库配置信息

传统的Web应用的数据库配置信息一般都是存放在WEB-INF目录下的 *.properties*.yml.xml中,如果是Spring Boot项目的话一般都会存储在jar包中的src/main/resources目录下。长剑的存储数据库配置信息的文件路径如:WEB-INF/applicationContext.xmlWEB-INF/hibernate.cfg.xmlWEB-INF/jdbc/jdbc.properties,一般情况下使用find命令加关键字可以轻松的找出来,如查找Mysql配置信息: find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"

对于windowslinux

windows通常放在安装目录下的MySQL\MySQL Server 5.0\my.ini

linux默认是放在/etc/my.cnf

为什么需要Class.forName

在最开始的时候,我不明白为什么第一步必须是Class.forName(CLASS_NAME);//注册JDBC驱动类

实际上,这一步是利用了Java反射+类加载机制往DriverManager中注册了驱动包。

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver{
	public Driver() throws SQLException{
	}
	
	static {
		try{
			DriverManager.registerDriver(new Driver());
		} cathc (SQLException var1){
			throw new RuntimeException("Can't register driver!");
		}
	}
}

Class.forName("com.mysql.jdbc.Driver")实际上会触发类加载,com.mysql.jdbc.Driver类将会被初始化,所以static静态语句块种的代码也将会被执行。如果反射某个类又不想初始化类方法有两种途径:

  1. 使用Class.forName("xxxx",false,loader)方法,将第二个参数传入false。
  2. ClassLoader.load("xxxx");

反射的概述

Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java预言的反射机制。

java类加载机制

类加载的时机

  • 隐式加载new创建类的实例
  • 显式加载:loaderClass.forName等
  • 访问类的静态变量,或者为静态变量赋值
  • 调用类的静态方法
  • 使用反射方法创建某个类或者接口对象的Class对象。
  • 初始化某个类的子类
  • 直接使用java.exe命令来 运行某个主类

类加载的过程

加载 -> 验证 -> 准备 -> 解析 -> 初始化

加载:类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象。

验证:目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。

准备:为变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i=5这里只是将i赋值为0,在初始化的阶段再把i赋值为5),这里不包括final修饰的static,因为final在编译的时候就已经分配了。这里不会为实例变量分配给初始化,类变量会分配在方法区中,实例变量会随着对象分配到java对中。

解析:这里主要的任务是把常量池中的符号引用替换成直接引用

初始化:这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对static初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)

类记载器的任务是根据类的全限定名来读取此类的二进制字节流到JVM中,然后转换成一个与目标类对象的java.lang.Class对象的实例,在java虚拟机提供三种类加载器,引导类加载器,扩展类加载器,系统类加载器。

Class.forName可以省去吗

连接数据库必须Class.forName(xxx)几乎已经成为了绝大部分人认为的既定事实而不可改变,但是某些人会发现删除Class.forName一样可以连接数据库。

实际上,这里又利用了Java的一大特性:Java SPI(Service Provider Interface),因为DriverManager在初始化的时候会调用java.util.ServiceLoader类提供的SPI机制,Java会自动扫描jar包中的META-INF/services目录下的文件,并且还会自动的Class.forName(文件中定义的类)

DataSource

DataSource又叫数据库连接池。一般情况下,我们不会在java项目中使用原生的jdbc的DriverManager连接数据库,而是使用数据库连接池来代替。这样做的好处是,我们只需要提前定义好数据库的配置文件,在我们重新写一个连接数据库程序的时候,就不需要再写连接数据库的代码,而是直接引用DataSource对象。

常见的数据连接池有c3p0,druid等,他们都实现于javax.sql.DataSource接口。

c3p0

步骤:

  1. 导入jar包
  2. 定义配置文件
    • 名称:c3p0.properties 或者 c3p0-config.xml
    • 路径:直接将文件放在src目录下即可。
  3. 创建核心对象,数据库连接池对象
package c3p0;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * c3p0的演示
 */
public class Demo01 {
    public static void main(String[] args) throws SQLException {
        //创建数据库连接池对象
        DataSource ds = new ComboPooledDataSource();
        //获取连接对象
        Connection conn = ds.getConnection();
        //打印
        System.out.println(conn);
    }
}

c3p0配置文件

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
	<default-config>
		//注册驱动
		<property name="driverClass"> 
			com.mysql.jdbc.Driver
		</property>
		//数据库地址
		<property name="jdbcUrl">
			jdbc:mysql:///db1
		</property>
		//数据库用户名
		<property name="user">
			root1
		</property>
		//数据库密码
		<property name="password">
			password
		</property>
		//连接池数量
		<property name="initialPoolSize">
			5
		</property>
		//最大连接数
		<property name="maxPoolSize">
			10
		</property>
		//超时时间
		<property name="checkoutTimeout">
			3000
		</property>
	</default-config>
</c3p0-config>

druid

步骤:

  1. 导入jar包

  2. 定义配置文件:

    • 是properties格式的
    • 可以叫任意名称,可以放在任意目录下
    driverClassName=com.mysql.jdbc.Driver
    url=jdbc:mysql:///db1
    username=root1
    password=password
    //初始化的连接数量
    initiaSize=5
    //最大连接数
    maxActive=10
    //最大等待时间
    maxWait=3000
    
  3. 加载配置文件

    Properties pro = new Properties();
            InputStream is = druid.Demo01.class.getClassLoader().getResourceAsStream("druid.properties");
            pro.load(is);
    
  4. 获取数据库连接池对象:通过工厂类来获取

  5. 获取连接

package druid;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

/**
 * Druid演示
 */

public class Demo01 {
    public static void main(String[] args) throws Exception {
        //加在配置文件
        Properties pro = new Properties();
        InputStream is = druid.Demo01.class.getClassLoader().getResourceAsStream("druid.properties");
        pro.load(is);
        //获取连接池对象
        DataSource ds = DruidDataSourceFactory.createDataSource(pro);
        //获取连接
        Connection conn = ds.getConnection();
        System.out.println(conn);
    }
}

Spring MVC

在Spring MVC中,我们可以自由的选择第三方数据源,通常我们会定义一个DataSource Bean用于配置和初始化数据源对象,然后在Spring中就可以通过Bean注入的方式获取数据源对象。

Bean注入的方式有两种,一种是在XML中配置,此时分别有属性注入、构造函数注入和工厂方法注入;另一种则是使用注解的方式注入@Autowired,@Resource,@Required

在基于XML配置的SpringMVC中配置数据源:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        ....
        />

如上,我们定义了一个id为dataSource的Spring Bean对象,usernamepassword都使用了${jdbc.XXX}表示,很明显${jdbc.username}并不是数据库的用户名,这其实是采用了Spring的property-placeholder制定了一个properties文件,使用${jdbc.username}其实会自动自定义的properties配置文件中的配置信息。

<context:property-placeholder location="classpath:/config/jdbc.properties"/>

jdbc.properties内容:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
jdbc.username=root
jdbc.password=root

在Spring中我们只需要通过引用这个Bean就可以获取到数据源了,比如在Spring JDBC中通过注入数据源(ref="dataSource")就可以获取到上面定义的dataSource

<!-- jdbcTemplate Spring JDBC 模版 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="false" lazy-init="false">
  <property name="dataSource" ref="dataSource"/>
</bean>

SpringBoot配置数据源:

在SpringBoot中只需要在application.propertiesapplication.yml中定义spring.datasource.xxx即可完成DataSource配置。

spring.datasource.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

Spring hack

我们通常可以通过查找Spring数据库配置信息找到数据库账号密码,但是很多时候我们可能会找到非常多的配置项甚至是加密的配置信息,这将会让我们非常的难以确定真是的数据库配置信息。某些时候在授权渗透测试的情况下我们可能会需要传个shell尝试性的连接数据库证明危害,那么可以在webshell中使用注入数据源的方式来获取数据库连接对象,甚至是读取数据库密码。

Java Web Server数据源

除了第三方数据源库实现,标准的Web容器自身也提供了数据源服务,通常会在容器中配置DataSource信息并注册到JNDI(Java Naming and Directory Interface)中,在Web应用中我们可以通过JNDI的接口lookup(定义的JNDI路径)来获取到DataSource对象。

Tomcat JNDI DataSource

Tomcat配置JNDI数据源需要手动修改Tomcat目录/conf/context.xml文件,参考:Tomcat JNDI Datasource

<Context>

  <Resource name="jdbc/test" auth="Container" type="javax.sql.DataSource"
               maxTotal="100" maxIdle="30" maxWaitMillis="10000"
               username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/mysql"/>

</Context>

Resin JNDI DataSource

Resin需要修改resin.xml,添加database配置,参考:Resin Database configuration

<database jndi-name='jdbc/test'>
  <driver type="com.mysql.jdbc.Driver">
    <url>jdbc:mysql://localhost:3306/mysql</url>
    <user>root</user>
    <password>root</password>
  </driver>
</database>

jdbc SQL

这里,为了验证我们之前写的代码是有sql注入风险的,所以我们可以写一个简单的数据查询页面。

Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/db1";
String name = "root1";
String password = "password";
Connection conn = DriverManager.getConnection(url,name,password);

首先第一步还是先获取数据库连接。

然后,因为这个查询我们需要有交互性,所以搜索的条件必须要从页面以get形式传入参数。

从页面获取参数的方法。

request.getParameter()

然后,我们需要将从页面获取来的参数传递到我们要进行数据库检索时候使用的代码中。

String user = request.getParameter("user");
user = "'"+user+"'";
String sql = "select * from user where name = "+user;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

这就完成了从页面 传参,然后到数据库中以传参的为条件进行查询这一项。

然后,我们就需要将数据提取,并且在页面上打印出来。

rs.next();
int id = rs.getInt(1);
String username = rs.getString("name");
String passwd = rs.getString("passwd");
response.getWriter().write(id+"--"+username+"--"+passwd);

这样,就完成了一个简单的从页面传参,然后到数据库内部查询的这么一个结果。

但是,这么做会有一个问题,那就是传入sql语句中的参数,是用户可控的,如果用户在sql语句中拼接恶意命令,我们是无法防御的,这就是我们常说的sql注入漏洞。

而应对这个问题,也有解决办法,就是对参数进行预编译。

简单来说,就是利用“?”占位符来代替我们直接传入到sql语句中的参数。

这样的话,我们sql语句执行的那段就会变成。

String sql = "select * from user where name = ?";
PreparedStatement preparedStatement = null;
preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1,user);
ResultSet resultSet = preparedStatement.executeQuery();

这个时候的页面依然可以完成我们正常的查询

但当我们再次对这个地址进行sql注入探测的时候,就会发现,sql注入问题已经消失。

posted @ 2020-12-08 23:27  小明-o3rr0r  阅读(107)  评论(0编辑  收藏  举报