一、sql注入概念
SQL注入攻击,简称SQL攻击或注入攻击,是发生于应用程序中数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了检查,那么这些注入进去的指令就会被数据库服务器误认为是正常的SQL指令而运行,因此遭到破坏或是入侵。
二、sql注入举例
最常见的就是我们在应用程序中使用字符串联结方式组合 SQL 指令,有心之人就会写一些特殊的符号,恶意篡改原本的 SQL 语法的作用,达到注入攻击的目的。比如验证用户登录需要 username 和 password,编写的 SQL 语句如下:
select * from user where (name = '"+ username +"') and (pw = '"+ password +"');
username 和 password 字段被恶意填入:username = "1' OR '1'='1"; 与 password = "1' OR '1'='1"; 将导致原本的 SQL 字符串被填为:
select * from user where (name = '1' or '1'='1') and (pw = '1' or '1'='1');
实际上运行的 SQL 语句将变成:
select * from user;
也就是不再需要 username 和 password 账密即达到登录的目的,结果不言而喻。
三、解决sql注入原理
利用预编译的机制将sql语句的主干和参数分别传输给数据库服务器,从而使数据库分辨的出哪些是sql语句的主干哪些是参数,这样一来即使参数中带了sql的关键字(比如上面的 "1' OR '1'='1"),数据库服务器也仅仅将他当作参数(比如上面的name与pw)的值使用,关键字不会起作用,从而从原理上防止了sql注入的问题。
四、JDBC中防止sql注入
使用PreparedStatement可以防止sq注入。
PreparedStatement主要有如下的三个优点:
1.可以防止sql注入
2.由于使用了预编译机制,执行的效率要高于Statement
3.sql语句使用?形式替代参数,然后再用方法设置?的值,比起拼接字符串,代码更加优雅.
PreparedStatement 与Statment比较
1)语法不同:PreparedStatement可以使用预编译的sql,而Statment只能使用静态的sql
2)效率不同: PreparedStatement可以使用sql缓存区,效率比Statment高
3)安全性不同: PreparedStatement可以有效防止sql注入,而Statment不能防止sql注入。
/**
* PreparedStatement執行sql語句
*
*
*/
public class Demo1 {
/**
* 增加
*/
@Test
public void testInsert() {
Connection conn = null;
PreparedStatement stmt = null;
try {
//1.获取连接
conn = JdbcUtil.getConnection();
//2.准备预编译的sql
String sql = "INSERT INTO student(NAME,gender) VALUES(?,?)"; //?表示一个参数的占位符
//3.执行预编译sql语句(检查语法)
stmt = conn.prepareStatement(sql);
//4.设置参数值
/**
* 参数一: 参数位置 从1开始
*/
stmt.setString(1, "李四");
stmt.setString(2, "男");
//5.发送参数,执行sql
int count = stmt.executeUpdate();
System.out.println("影响了"+count+"行");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn, stmt);
}
}
/**
* 修改
*/
@Test
public void testUpdate() {
Connection conn = null;
PreparedStatement stmt = null;
try {
//1.获取连接
conn = JdbcUtil.getConnection();
//2.准备预编译的sql
String sql = "UPDATE student SET NAME=? WHERE id=?"; //?表示一个参数的占位符
//3.执行预编译sql语句(检查语法)
stmt = conn.prepareStatement(sql);
//4.设置参数值
/**
* 参数一: 参数位置 从1开始
*/
stmt.setString(1, "王五");
stmt.setInt(2, 9);
//5.发送参数,执行sql
int count = stmt.executeUpdate();
System.out.println("影响了"+count+"行");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn, stmt);
}
}
/**
* 删除
*/
@Test
public void testDelete() {
Connection conn = null;
PreparedStatement stmt = null;
try {
//1.获取连接
conn = JdbcUtil.getConnection();
//2.准备预编译的sql
String sql = "DELETE FROM student WHERE id=?"; //?表示一个参数的占位符
//3.执行预编译sql语句(检查语法)
stmt = conn.prepareStatement(sql);
//4.设置参数值
/**
* 参数一: 参数位置 从1开始
*/
stmt.setInt(1, 9);
//5.发送参数,执行sql
int count = stmt.executeUpdate();
System.out.println("影响了"+count+"行");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn, stmt);
}
}
/**
* 查询
*/
@Test
public void testQuery() {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
//1.获取连接
conn = JdbcUtil.getConnection();
//2.准备预编译的sql
String sql = "SELECT * FROM student";
//3.预编译
stmt = conn.prepareStatement(sql);
//4.执行sql
rs = stmt.executeQuery();
//5.遍历rs
while(rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
String gender = rs.getString("gender");
System.out.println(id+","+name+","+gender);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
//关闭资源
JdbcUtil.close(conn,stmt,rs);
}
}
}
测试代码:
/**
* 模拟用户登录效果
*
*
*/
public class Demo2 {
//模拟用户输入
//private String name = "ericdfdfdfddfd' OR 1=1 -- ";
private String name = "eric";
//private String password = "123456dfdfddfdf";
private String password = "123456";
/**
* Statment存在sql被注入的风险
*/
@Test
public void testByStatement(){
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
//获取连接
conn = JdbcUtil.getConnection();
//创建Statment
stmt = conn.createStatement();
//准备sql
String sql = "SELECT * FROM users WHERE NAME='"+name+"' AND PASSWORD='"+password+"'";
//执行sql
rs = stmt.executeQuery(sql);
if(rs.next()){
//登录成功
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn, stmt ,rs);
}
}
/**
* PreparedStatement可以有效地防止sql被注入
*/
@Test
public void testByPreparedStatement(){
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
//获取连接
conn = JdbcUtil.getConnection();
String sql = "SELECT * FROM users WHERE NAME=? AND PASSWORD=?";
//预编译
stmt = conn.prepareStatement(sql);
//设置参数
stmt.setString(1, name);
stmt.setString(2, password);
//执行sql
rs = stmt.executeQuery();
if(rs.next()){
//登录成功
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn, stmt ,rs);
}
}
}
五、mybatis中防止sql注入
mybatis 解决 SQL 注入问题
我们使用 mybatis 编写 SQL 语句时,难免会使用模糊查询的方法,mybatis 提供了两种方式 #{}
和 ${}
。
#{value}
在预处理时,会把参数部分用一个占位符 ? 替代,其中 value 表示接受输入参数的名称。能有效解决 SQL 注入问题${}
表示使用拼接字符串,将接受到参数的内容不加任何修饰符拼接在 SQL 中,使用${}
拼接 sql,将引起 SQL 注入问题。
#{} 只是替换?,相当于PreparedStatement使用占位符去替换参数,可以防止sql注入。
${} 是进行字符串拼接,相当于sql语句中的Statement,使用字符串去拼接sql;$可以是sql中的任一部分传入到Statement中,不能防止sql注入
举个例子:
1 查询数据库 sample 表 user 中的记录,我们故意使用特殊符号,看能否引起 SQL 注入。使用 mybatis 在 mapper.xml 配置文件中编写 SQL 语句,我们先采用拼接字符串形式,看看结果如何:
<select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
<!-- 拼接 MySQL,引起 SQL 注入 -->
SELECT * FROM user WHERE username LIKE '%${value}%'
</select>
注意在配置文件中编写 SQL 语句时,后边不需要加分号。
调用配置文件,编写测试文件,查询数据库内容,采用特殊符号,引起 SQL 注入:
@Test
public void testFindUserByName() throws Exception{
SqlSession sqlSession=sqlSessionFactory.openSession();
//创建UserMapper代理对象
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
//调用userMapper的方法
List<User> list=userMapper.findUserByName("' or '1'='1");
sqlSession.close();
System.out.println(list);
}
}
运行结果如下图所示:
可以看到执行语句其实变为了
select * from user
将user 表中的全部记录打印出来了。发生了 SQL 注入。
2 如果将配置文件中的 SQL 语句改成 #{}
形式,可避免 SQL 注入。
<select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
<!-- 使用 SQL concat 语句,拼接字符串,防止 SQL 注入 -->
SELECT * FROM USER WHERE username LIKE CONCAT('%',#{value},'%' )
</select>
再次运行测试程序,控制台输出如下:
可以看到程序中参数部分用 ? 替代了,很好地解决了 SQL 语句的问题,防止了 SQL 注入。查询结果将为空。
六、代码地址
https://github.com/mazhongjia/mybatis-common