SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。
以模拟登录为例:在前台输入用户名和密码,后台判断信息是否正确,并给出前台反馈信息,前台输出反馈信息。
public class TestInjection { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static String user="root"; private static String password="root"; public static void main(String[] args) { Scanner sc =new Scanner(System.in); System.out.println("请输入用户名"); String username=sc.next(); System.out.println("请输入密码"); String pwd =sc.next(); Account account = getAccount(username, pwd); System.out.println(null!= account?"登录成功":"登录失败"); sc.close(); } public static Account getAccount(String username,String pwd){ Connection connection = null; Statement statement=null; ResultSet resultSet=null; Account account =null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); statement = connection.createStatement(); String sql="select * from account where username ='"+username+"' and password ='"+pwd+"'"; System.out.println(sql); resultSet = statement.executeQuery(sql); while(resultSet.next()){ int aid = resultSet.getInt("aid"); String usernamea = resultSet.getString("username"); String pwda = resultSet.getString("password"); double money = resultSet.getDouble("money"); account=new Account(aid,usernamea,pwda,money); System.out.println(account); } }catch (Exception e){ e.printStackTrace(); }finally { if(null != resultSet){ try { resultSet.close(); } catch (SQLException e) {e.printStackTrace(); } } if(null != statement){ try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } return account; } }
当输入下面的值时:
username=aaa,pwd=abc'or'aa'='a
即使是错误的,也能登录成功。让登录功能形同虚设,这就是SQL注入风险。这是为什么呢?原因在于SQL语句是字符串拼接的。SQL语句中拼接的内容破坏了SQL语句原有的判断逻辑。
public class TestInjection2 { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static String user="root"; private static String password="root"; public static void main(String[] args) { Scanner sc =new Scanner(System.in); System.out.println("请输入用户名"); String username=sc.next(); System.out.println("请输入密码"); String pwd =sc.next(); Account account = getAccount(username, pwd); System.out.println(null!= account?"登录成功":"登录失败"); sc.close(); } public static Account getAccount(String username,String pwd){ Connection connection = null; PreparedStatement preparedStatement=null; ResultSet resultSet=null; Account account =null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); /* * 1使用PreparedStatement语句对象防止注入攻击 * 2PreparedStatement 可以使用 ? 作为参数的占位符 * 3使用?作为占位符,即使是字符串和日期类型,也不使用单独再添加 '' * 4connection.createStatement();获得的是普通语句对象 Statement * 5connection.prepareStatement(sql);可以获得一个预编译语句对象PreparedStatement * 6如果SQL语句中有?作为参数占位符号,那么要在执行CURD之前先设置参数 * 7通过set***(问号的编号,数据) 方法设置参数 * */ String sql="select * from account where username = ? and password = ?"; preparedStatement = connection.prepareStatement(sql);//这里已经传入SQL语句 //设置参数 preparedStatement.setString(1,username ); preparedStatement.setString(2,pwd ); //执行CURD resultSet = preparedStatement.executeQuery();// 这里不需要再传入SQL语句 while(resultSet.next()){ int aid = resultSet.getInt("aid"); String usernamea = resultSet.getString("username"); String pwda = resultSet.getString("password"); double money = resultSet.getDouble("money"); account=new Account(aid,usernamea,pwda,money); System.out.println(account); } }catch (Exception e){ e.printStackTrace(); }finally { if(null != resultSet){ try { resultSet.close(); } catch (SQLException e) {e.printStackTrace(); } } if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } return account; } }
prepareStatment对象在set***方法上,会对单引号进行转译处理,也就是说,?中的数据的单引号 ‘ 会被转义成 \’,这样就单引号就不会破坏sql语句的结构。
SELECT * FROM users WHERE userName = ? AND password = ? preparedStatement.setString(1,"xiaoming"); preparedStatement.setString(2,'anything' OR 'x'='x'); 会被转义为 SELECT * FROM users WHERE userName = 'xiaoming' AND password = 'anything\' OR\'x\'=\'x\'' 而不是 SELECT * FROM users WHERE userName = 'xiaoming' AND password = 'anything' OR 'x'='x'
PreparedStatement把值当中的所有单引号给转义了,这就达到了防止sql注入的目的,mysql驱动的PreparedStatement实现类的setString();方法内部做了单引号的转义。
Statement不能防止sql注入,因为它没有把单引号做转义,而是直接拼接字符串,所以达不到防止sql注入的目的。