目 录(本篇字数:2841)
SQL注入攻击
SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令,从而利用系统的 SQL 引擎完成恶意行为的做法 。
SQL注入攻击,指对数据库进行攻击的手段之一。例如:在表单提交时候,需要操作数据库去匹配用户信息时,在 sql 语句中注入一些代码,去获取数据库信息或权限。接着,我们来看看在SQL注入代码,成功入侵数据库的案例。
Statement :罪魁祸首
Statement 是 Jdbc 提供的一个可以操作数据库的方法,通常用于增、删、改、查;用它的 statement.executeQuery(sql) 方法可以执行数据库语句。
案例关键代码如下:
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
Connection conn = null;
Statement statement = null;
ResultSet result = null;
PrintWriter out = resp.getWriter();
String user = req.getParameter("user");
String pwd = req.getParameter("pwd");
System.out.println("请求:user:" + user);
System.out.println("请求:pwd:" + pwd);
try {
conn = JdbcUtils.getConnection();
statement = conn.createStatement();
String sql = "SELECT * FROM user WHERE(user_name='" + user + "' AND password='" + pwd + "')";
System.out.println(sql);
result = statement.executeQuery(sql);
//result 返回 true 说明匹配成功
if (result.next()) {
out.println("user,login succeed");
} else {
out.println("login failed");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtils.release(statement, conn, result);
}
}
}
如上代码中的 sql 语句: SELECT * FROM user WHERE(user_name='" + user + "' AND password='" + pwd + "'),看似没有任何问题,但配合着 Statement 就会出现漏洞了。接着我们来测试 SQL 注入。
测试 SQL 注入攻击
我的数据库有一张 user 表如下:
我们使用表中的任何 user_name 和 password 都是可以登入的。接着,是用注入代码的方式: user 和 密码 都为
1' OR '1'='1,也是可以登入成功的。
sql 语句变成如上图控制台打印出来的那样,我们拿到数据库软件中跑一下,也是可以查出来。后面的 ‘1’ = ‘1’ 为真,这句 sql 语句其实变成了 SELECT * FROM user ,结果也就是查询出 user 所有数据信息。
漏洞原因
出现这种情况的原因是,每次在请求登入的时候时, Statement 在执行 sql 语句时都会把表单提交来的 用户名 和 密码 都重新拼接到 sql 语句中,然后在执行。这样会导致 sql 语句结构发生改变,攻击者利用这种方式攻击数据库就称为 SQL注入攻击。
PreparedStament 化解 SQL 注入
PreparedStament 是 Statement 的子接口,它最大特点就是预处理 sql 语句。它在实例化时,必须传入 sql 语句,这与Statement 就有所区别。sql 语句中使用占位符 ? 来代替未知参数值,继而通过 preparedStatement.setXXX() 方法添加进 sql 语句中,这样就不会改变 sql 的结构,就能防止 SQL 注入攻击了。
代码修改为:
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
Connection conn = null;
PreparedStatement preparedStatement = null;
ResultSet result = null;
PrintWriter out = resp.getWriter();
String user = req.getParameter("user");
String pwd = req.getParameter("pwd");
System.out.println("请求:user:" + user);
System.out.println("请求:pwd:" + pwd);
try {
conn = JdbcUtils.getConnection();
String sql = "SELECT * FROM user WHERE(user_name=? AND password=?)";
preparedStatement = conn.prepareStatement(sql);
System.out.println(sql);
preparedStatement.setString(1, user);
preparedStatement.setString(2, pwd);
result = preparedStatement.executeQuery();
//result 返回 true 说明匹配成功
if (result.next()) {
out.println("user,login succeed");
} else {
out.println("login failed");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtils.release(preparedStatement, conn, result);
}
}
}
测试结果,使用 1' OR '1'='1无法登入。
所以,在日常开发中一般都使用 PreparedStatement ,它不仅可以防止 SQL 注入,同时能够预编译 sql 语句,效率更佳。
JDBC 增、删、改、查
数据库为上文 user 表(图在上面)
JDBC 连接工具类public class JdbcUtils {
public static Connection getConnection() throws Exception {
String driver = null;
String jdbcUrl = null;
String user = null;
String password = null;
InputStream inputStream = JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties properties = new Properties();
properties.load(inputStream);
driver = properties.getProperty("driver");
jdbcUrl = properties.getProperty("jdbcUrl");
user = properties.getProperty("user");
password = properties.getProperty("password");
Class.forName(driver);
return (Connection) DriverManager.getConnection(jdbcUrl, user, password);
}
public static void release(Statement statement, Connection conn, ResultSet result) {
try {
if (statement != null) {
statement.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (result != null) {
result.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
增、删、改方法public class JdbcTest {
/**
* 提供: 增 、删、改3个功能的通用方法
*
* @param sql
* @param args sql 中占位符的值,可以用多个逗号隔开
*/
public void update(String sql, Object... args) throws Exception {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JdbcUtils.getConnection();
System.out.println(sql);
preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
preparedStatement.setObject(i + 1, args[i]);
}
preparedStatement.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtils.release(preparedStatement, connection, null);
}
}
@Test
public void insert() throws Exception {
String sql = "INSERT INTO user(user_name,sex,user_role,password,id_card,register_time)"
+ " VALUES(?,?,?,?,?,?)";
update(sql,"小红","女","VIP用户","xh","654...","2019-01-25 14:02:03");
}
@Test
public void delete() throws Exception {
// 删除 user 表中 user_name 为小红的信息
String sql = "DELETE FROM user WHERE user_name=?";
update(sql,"小红");
}
@Test
public void modify() throws Exception {
// user 表中 user_name 为小红 的用户,将其 user_role 改为 普通用户
String sql = "UPDATE user set user_role=? WHERE user_name=?";
update(sql,"普通用户","小红");
}
}
通用查询方法封装
利用面向对象编程的思想,我们新建了一个 User 类,此类变量为 user 表对应的列名,代码如下:
package com.xww;
public class User {
public String userName;
public String password;
public String registerTime;
public String sex;
public String userRole;
public String idCard;
public User(String userName, String password, String registerTime,
String sex, String userRole, String idCard) {
super();
this.userName = userName;
this.sex = sex;
this.userRole = userRole;
this.password = password;
this.idCard = idCard;
this.registerTime = registerTime;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getUserRole() {
return userRole;
}
public void setUserRole(String userRole) {
this.userRole = userRole;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
public String getRegisterTime() {
return registerTime;
}
public void setRegisterTime(String registerTime) {
this.registerTime = registerTime;
}
@Override
public String toString() {
return "User [userName=" + userName + ", password=" + password + ", registerTime=" + registerTime + ", sex="
+ sex + ", userRole=" + userRole + ", idCard=" + idCard + "]";
}
}
那么查询 user 表的方法就可以写为这样(不够灵活):
public User query(String sql) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
User user = null;
try {
connection = JdbcUtils.getConnection();
System.out.println(sql);
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
user = new User(resultSet.getString(2), resultSet.getString(3), resultSet.getString(4),
resultSet.getString(5), resultSet.getString(6), resultSet.getString(7));
System.out.println(user.toString());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtils.release(preparedStatement, connection, resultSet);
}
return user;
}
这样的确可以获取到 user 表,但是这个方法并不能通用。例如:我想查询 student 表时,student 表中字段名与 user 表肯定不一样,这样就会产生错误。所以,这种情况就可以利用 Java 反射来实现。可是,User 类中的变量相当于 user 表中的列名,如果让 User 类将变量名强制写成 user 表中一样,那就显得很局限,不能够通配。那么,就得用到 JDBC 提供的另一个接口了。
(1)ResultSetMetaData 接口
它用于获取 ResultSet 结果集的元数据,用处是在于从查出的结果集中获取相应的列数、列名等。常用方法如下:
getColumnName(int column):获取指定列的名称
getColumnCount():返回当前 ResultSet 对象中的列数。
getColumnTypeName(int column):检索指定列的数据库特定的类型名称。
getColumnDisplaySize(int column):指示指定列的最大标准宽度,以字符为单位。
isNullable(int column):指示指定列中的值是否可以为 null。
isAutoIncrement(int column):指示是否自动为指定列进行编号,这样这些列仍然是只读的。
(2)利用 sql 语句添加别名的方式
比如我要查询 user 表中 ‘小王’ 的详细信息,原本的 sql 语句:
> SELECT * FROM user WHERE user_name='小王'
要改为添加别名的方式,注意:别名一定是与User类中定义变量名一致,否则在反射时将找不到字段:
> SELECT user_name userName,password password,register_time registerTime,sex sex,user_role userRole,id_card idCard FROM user WHERE user_name='小王'
查出来的结果如下图:
字段名都与 User 类中的变量名相一致了。
(3)利用 Java 反射给 User 类中的每个属性变量赋予值 .
那么,经过这几翻的处理,我们的 User 类终于可以匹配数据库了。这个虽说可以实现通用查询,映射到实体类中,但明显在 sql 语句中那么点缺乏简练。整个查询代码如下:
public class JdbcTest {
/**
* 利用 Java 反射机制,写的一个通用查询方法
*
* @param sql
*/
public T query(Class clazz, String sql, Object... args) {
T entity = null;
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
User user = null;
try {
connection = JdbcUtils.getConnection();
//System.out.println(sql);
preparedStatement = connection.prepareStatement(sql);
if (args != null) {
for (int i = 0; i < args.length; i++) {
preparedStatement.setObject(i + 1, args[i]);
}
}
resultSet = preparedStatement.executeQuery();
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
Map values = new HashMap();
if (resultSet.next()) {
for (int i = 0; i < resultSetMetaData.getColumnCount(); i++) {
String columnLable = resultSetMetaData.getColumnLabel(i + 1);
Object columnValue = resultSet.getObject(i + 1);
values.put(columnLable, columnValue);
}
}
System.out.println("--->数据库:"+values);
if (values.size() > 0) {
entity = (T) clazz.newInstance();
for (Entry entry : values.entrySet()) {
String fieldName = entry.getKey();
Object fieldValue = entry.getValue();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(entity, fieldValue);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtils.release(preparedStatement, connection, resultSet);
}
return entity;
}
@Test
public void qurey() {
String sql = "SELECT user_name userName,password password,register_time registerTime,"
+ "sex sex,user_role userRole,id_card idCard FROM user WHERE user_name=?";
User user = query(User.class, sql, "小王");
System.out.println("--->反射到 User 类中:"+user.toString());
}
}
(4)BeanUtils 工具包
BeanUtils 工具包是 Apache 出的一个辅助工具,主要方便对于 JavaBean 进行一系列的操作。例如:
可以对 JavaBean 的 getter()/setter() 方法进行赋值操作
可以将Map集合对象直接转为 JavaBean 对象,需要属性名一致
可以实现两个 JavaBean 对象的拷贝操作
BeanUtils 工具类的常用方法如下:
BeanUtils.setProperty(bean, name, value);实现对象的赋值操作
ConvertUtils.register(Converter converter , ..);当需要将String数据转换成引用数据类型(自定义数据类型时),需要使用此方法实现转换
BeanUtils.populate(bean,Map);实现将Map拷贝到对象中
BeanUtils.copyProperties(newObject,oldObject);实现对象之间的拷贝
现在我们导入 beanutils.jar 包和一个依赖的 loggin.jar 包:
修改上面部分代码:
将 Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(entity, fieldValue);
修改为 BeanUtils.setProperty(entity, fieldName, fieldValue);
运行代码,匹配数据库成功,并反射到 User 类中: