JDBC(Java Database Connectivity)
是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(JDBC
),不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。
JDBC Connection Connection流程 Java通过java.sql.DriverManager
来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager
中注册对应的驱动类,然后调用getConnection
方法才能连接上数据库。
JDBC定义了一个叫做java.sql.Driver
的接口类负责对数据库的连接,数据库驱动包必须自己去实现这个接口才能完成数据库的连接操作,java.sql.DriverManager
的getConnection
方法实际上就是调用了java.sql.Driver
的connect方法实现连接,连接成功后返回一个java.sql.Connection
的数据库连接对象,以后一切的数据库操作都依赖这个对象。
可以看到getConnection方法经历了很多重载方法的调用,主要就是要把user,password包进info,另外从这个函数需要的参数可以知道我们需要传入url,user和password来实现数据库连接。
在最后调用的getConnection中可以看到先检测了驱动是否注册,后尝试调用connect方法来连接数据库。
总的来说:JDBC连接数据库的一般步骤:
注册驱动,Class.forName("数据库驱动的类名")
。
获取连接,DriverManager.getConnection(xxx)
。
JDBC连接数据库示例代码如下:
1 2 3 4 5 6 7 String CLASS_NAME = "com.mysql.jdbc.Driver" ;String URL = "jdbc:mysql://localhost:3306/mysql" String USERNAME = "root" ;String PASSWORD = "root" ;Class.forName(CLASS_NAME); Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
数据库配置信息 传统的Web应用的数据库配置信息一般都是存放在WEB-INF
目录下的*.properties
、*.yml
、*.xml
中的,如果是Spring Boot
项目的话一般都会存储在jar包中的src/main/resources/
目录下。常见的存储数据库配置信息的文件路径如:WEB-INF/applicationContext.xml
、WEB-INF/hibernate.cfg.xml
、WEB-INF/jdbc/jdbc.properties
,一般情况下使用find命令加关键字可以轻松的找出来,如查找Mysql配置信息: find 路径 -type f |xargs grep "com.mysql.jdbc.Driver"
为何需要forName 为了给出这个问题的答案,我们可以先去找一个数据库的驱动类来分析:
1 2 3 4 5 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.37</version > </dependency >
搭好之后找到com.mysql.jdbc.Driver,这个就是mysql数据库的驱动类:
可以看到这里是有一块静态代码块的,而结合我们之前学过的类加载知识,当Class.forName(CLASS_NAME)实际上会触发显式类加载,这个类会被初始化,静态代码块会自动执行,再仔细看看这个代码,就是往DriverManager里面注册了这个驱动包。
也就是说实际上forName这一步是利用了Java反射+类加载机制往DriverManager
中注册了驱动包。
另外说一句,如果只是想反射而不想初始化,需要使用Class.forName(“xxxx”, false, loader)方法,将第二个参数传入false,或者ClassLoader.load(“xxxx”);
为何forName可以省去 刚刚说了forName这么重要,注册驱动包,现在又说可以省去??别急,是因为Java的SPI机制帮你自动完成了forName,所以我们写代码的时候可以省略不写。
因为DriverManager
在初始化的时候会调用java.util.ServiceLoader
类提供的SPI机制,Java会自动扫描jar包中的META-INF/services
目录下的文件,并且还会自动进行Class.forName(文件中定义的类)
JDBC DataSource 配置数据源,就是建立与数据库的连接,常见的数据源有hikariCP、druid、tomcat-jdbc、dbcp、c3p0等,他们都实现了javax.sql.DataSource
提供的接口,那么为什么要用数据源去建立与数据库的连接,而不是用我们上面提到的jdbc呢,原因之一就是使用jdbc建立连接和销毁太过于浪费资源。
一般情况下在Web服务启动时候会预先定义好数据源,有了数据源程序就不再需要编写任何数据库连接相关的代码了,直接引用DataSource
对象即可获取数据库连接了。
下面的spring框架相关这里先做记录,以后再详细分析。
Spring MVC 数据源 在Spring MVC中我们可以自由的选择第三方数据源,通常我们会定义一个DataSource Bean
用于配置和初始化数据源对象,然后在Spring中就可以通过Bean注入的方式获取数据源对象了。
在基于XML配置的SpringMVC中配置数据源:
1 2 3 4 5 6 <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}" /> .... />
SpringBoot配置数据源:
在SpringBoot中只需要在application.properties
或application.yml
中定义spring.datasource.xxx
即可完成DataSource配置。
1 2 3 4 5 spring.datasource.url=jdbc:mysql: 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 这里给出一个脚本,通过在webshell中使用注入数据源的方式来获取数据库连接对象,进而获取数据库信息或者执行SQL语句:
spring-datasource.jsp
获取数据源/执行SQL语句示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="org.springframework.context.ApplicationContext" %> <%@ page import ="org.springframework.web.context.support.WebApplicationContextUtils" %> <%@ page import ="javax.sql.DataSource" %> <%@ page import ="java.sql.Connection" %> <%@ page import ="java.sql.PreparedStatement" %> <%@ page import ="java.sql.ResultSet" %> <%@ page import ="java.sql.ResultSetMetaData" %> <%@ page import ="java.util.List" %> <%@ page import ="java.util.ArrayList" %> <%@ page import ="java.lang.reflect.InvocationTargetException" %> <style> th, td { border: 1px solid #C1DAD7; font-size: 12px; padding: 6px; color: #4f6b72; } </style> <%! private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource" ; private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource" ; private static final String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource" ; List<DataSource> getDataSources (ApplicationContext ctx) { List<DataSource> dataSourceList = new ArrayList <DataSource>(); String[] beanNames = ctx.getBeanDefinitionNames(); for (String beanName : beanNames) { Object object = ctx.getBean(beanName); if (object instanceof DataSource) { dataSourceList.add((DataSource) object); } } return dataSourceList; } String printDataSourceConfig (ApplicationContext ctx) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { List<DataSource> dataSourceList = getDataSources(ctx); for (DataSource dataSource : dataSourceList) { String className = dataSource.getClass().getName(); String url = null ; String UserName = null ; String PassWord = null ; if (C3P0_CLASS_NAME.equals(className)) { Class clazz = Class.forName(C3P0_CLASS_NAME); url = (String) clazz.getMethod("getJdbcUrl" ).invoke(dataSource); UserName = (String) clazz.getMethod("getUser" ).invoke(dataSource); PassWord = (String) clazz.getMethod("getPassword" ).invoke(dataSource); } else if (DBCP_CLASS_NAME.equals(className)) { Class clazz = Class.forName(DBCP_CLASS_NAME); url = (String) clazz.getMethod("getUrl" ).invoke(dataSource); UserName = (String) clazz.getMethod("getUsername" ).invoke(dataSource); PassWord = (String) clazz.getMethod("getPassword" ).invoke(dataSource); } else if (DRUID_CLASS_NAME.equals(className)) { Class clazz = Class.forName(DRUID_CLASS_NAME); url = (String) clazz.getMethod("getUrl" ).invoke(dataSource); UserName = (String) clazz.getMethod("getUsername" ).invoke(dataSource); PassWord = (String) clazz.getMethod("getPassword" ).invoke(dataSource); } return "URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>" ; } return null ; } %> <% String sql = request.getParameter("sql" ); ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext()); List<DataSource> dataSourceList = getDataSources(ctx); if (dataSourceList == null ) { out.println("未找到任何数据源配置信息!" ); return ; } out.println("<hr/>" ); out.println("Spring DataSource配置信息获取测试:" ); out.println("<hr/>" ); out.print(printDataSourceConfig(ctx)); out.println("<hr/>" ); sql = sql != null ? sql : "select version()" ; for (DataSource dataSource : dataSourceList) { out.println("<hr/>" ); out.println("SQL语句:<font color='red'>" + sql + "</font>" ); out.println("<hr/>" ); Connection connection = dataSource.getConnection(); PreparedStatement pstt = connection.prepareStatement(sql); ResultSet rs = pstt.executeQuery(); out.println("<table><tr>" ); ResultSetMetaData metaData = rs.getMetaData(); for (int i = 1 ; i <= metaData.getColumnCount(); i++) { out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")\t" + "</th>" ); } out.println("<tr/>" ); while (rs.next()) { out.println("<tr>" ); for (int i = 1 ; i <= metaData.getColumnCount(); i++) { out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>" ); } out.println("<tr/>" ); } rs.close(); pstt.close(); } %>
上面的代码不需要手动去配置文件中寻找任何信息就可以直接读取出数据库配置信息甚至是执行SQL语句,其实是利用了Spring的ApplicationContext
遍历了当前Web应用中Spring管理的所有的Bean,然后找出所有DataSource
的对象,通过反射读取出C3P0
、DBCP
、Druid
这三类数据源的数据库配置信息,最后还利用了DataSource
获取了Connection
对象实现了数据库查询功能。
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
文件
1 2 3 4 5 6 7 8 <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
配置
1 2 3 4 5 6 7 <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注入可以说是大家都不陌生,各种注入方式也是层出不穷。今天主要想分析一下java代码中如何对sql注入进行防御。
通常情况下有以下方式来防御sql注入攻击:
加黑名单过滤(事实证明,此法一般还是会被突破)
限制用户传入的数据类型,如果预期传入的是数字,就使用Integer.parseInt()/Long.parseLong
强转
使用PreparedStatement
对象提供的SQL语句预编译(转义单引号)。
PreparedStatement SQL预编译查询 将存在注入的Java代码改为?(问号)
占位的方式即可实现SQL预编译查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 String id = request.getParameter("id" );String sql = "select id, username, email from sys_user where id =? " ;PreparedStatement pstt = connection.prepareStatement(sql);pstt.setObject(1 , id); ResultSet rs = pstt.executeQuery()
将用户传入的变量先用问号表示,然后对sql语句进行预编译,再把变量设置成参数值,这样就可以防御sql注入。
其实由最终执行的 SQL 可以看出,PreparedStatement 防止 SQL 注入的原理就是把用户非法输入的单引号进行转义,最终传入参数作为一个整体 执行,这样就不会让用户输入的数据影响问号后面的语句。
JDBC预编译 在jdbc使用PreparedStatement对象的sql语句实现预编译有两种方式,一种是客户端预编译,另一种是服务器端预编译,对应的URL配置项是:useServerPrepStmts
,当useServerPrepStmts
为false
时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmts
为true
时使用数据库服务器端预编译。
当使用数据库服务器预编译的时候,本地抓查询数据包会发现没有发生编译,而使用客户端预编译的时候,可以看到数据包中已经对sql语句进行了转义。
客户端预编译调用的是PreparedStatement里面的setString方法。
Mysql预编译 Mysql默认提供了预编译命令:prepare
,使用prepare
命令可以在Mysql数据库服务端实现预编译查询。
prepare
查询示例:
1 2 3 prepare stmt from 'select host,user from mysql.user where user = ?' ;set @username = 'root' ;execute stmt using @username ;
总结 :本文主要研究了java如何连接数据库,以及sql注入的防御。