一、SQL注入的含义:
SQL注入是一种常见的网络攻击,由于程序对输入数据的判断或者检验不严格,导致攻击者查询到了授权范围之外的数据,甚至还可以修改数据库中的数据,对数据库执行一些管理操作等,所以它的危害性也是比较大的。
像下面这个SQL,程序的本意是查询id=1的用户信息,但如果输入的参数后面又添加了OR 1=1,这样就会把所有的用户信息搜出来,显然,这是我们需要避免的:
SELECT * FROM t_user WHERE id = 1 OR 1=1;
二、实例演示:
首先创建一个测试表,并插入简单的数据(MySQL数据库):
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(30) DEFAULT NULL,
`password` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `t_user` VALUES ('1', 'John', '111111');
INSERT INTO `t_user` VALUES ('2', 'Tom', '222222');
接下来是Java程序部分,采用Spring Boot加上MyBatis的方式,如果对于搭建Spring Cloud工程还不太熟悉,可以参考之前的文章:手把手:Spring Cloud Alibaba项目搭建
在pom.xml文件中添加依赖:
<!-- 连接Spring Boot和MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
然后是Java各层的代码,从controller一直到dao,为了完整起见,我把代码都贴出来。controller层有一个方法,参数为用户名,返回该用户的详细信息,为了演示方便,此处返回一个列表:
package com.fullstack.commerce.user.controller;
import com.fullstack.commerce.user.entity.User;
import com.fullstack.commerce.user.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("user")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("getUserInfo")
@ResponseBody
// 根据用户姓名查询出用户列表信息
public List<User> getUserInfo(@RequestParam("username")String username){
List<User> result = userService.getUserInfo(username);
return result;
}
}
service层比较简单,有一个接口和对应的实现类,而实现类就是调用dao的方法,把用户列表查询出来:
package com.fullstack.commerce.user.service;
import com.fullstack.commerce.user.entity.User;
import java.util.List;
public interface UserService {
List<User> getUserInfo(String username);
}
package com.fullstack.commerce.user.service.impl;
import com.fullstack.commerce.user.dao.UserDao;
import com.fullstack.commerce.user.entity.User;
import com.fullstack.commerce.user.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
@Override
public List<User> getUserInfo(String username) {
return userDao.getUserInfo(username);
}
}
dao层就是一个接口,里面只有一个方法:
package com.fullstack.commerce.user.dao;
import com.fullstack.commerce.user.entity.User;
import java.util.List;
public interface UserDao {
List<User> getUserInfo(String username);
}
除了上面的业务代码,还需要有一个启动类:
package com.fullstack.commerce.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.fullstack.commerce.user.dao")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
以下是对应的mapper文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fullstack.commerce.user.dao.UserDao">
<resultMap id="UserMap" type="com.fullstack.commerce.user.entity.User">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
</resultMap>
<select id="getUserInfo" resultMap="UserMap">
SELECT * FROM t_user WHERE username = ${username}
</select>
</mapper>
注意,在上面这个mapper文件中,只有一个select语句,而这个语句传参使用了符号$,它会把参数值直接进行替换,而不会进行预编译(如果使用占位符#,就会进行预编译,从而可以防止SQL注入)。
当然,application.yml文件也需要配置一下,数据库用户名和密码替换成实际的:
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: myuser
password: myuser
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: mapper/*.xml
代码就算写完了,接下来我们测试一下看看。
三、测试验证:
启动程序,我们来进行测试,在浏览器里面输入以下地址,然后回车:
http://localhost:8080/user/getUserInfo?username='John'
没问题,我们是要查找用户名为John的数据,它返回了如下的结果,正是我们所期望的:
但如果我们把参数重新设置一下,变成如下这样:
http://localhost:8080/user/getUserInfo?username='John' OR 1=1
那就相当于在数据库中执行下面的SQL了,也就是返回所有的用户数据:
SELECT * FROM t_user WHERE username = 'John' OR 1=1
看结果,确实是这样,把表中的两条记录都返回到了客户端(测试数据只插了两条记录,如果有多条,同样会把所有的数据都返回过来):
这显然是不对的,接口把查询范围之外的数据都搜索出来了,如果还有一些修改数据或者操纵数据库的一些命令,那就更危险了。
当然,我们把mapper文件里面的参数部分改成占位符#,这样就会先进行预编译,就不会发生刚才的情况了。修改成#以后运行程序,再执行上面的url,就会返回空了,因为这个时候是去数据库中查询用户名为【'John' OR 1=1】的记录,显然是不存在的。
四、总结:
SQL注入是比较常见的一种攻击方式,如果ORM框架选择了MyBatis,处理这种问题的方式也相对比较简单,就是对于参数的处理要使用占位符,而不是直接把用户的输入进行简单的拼接。
本文主要是介绍SQL注入的基本概念,并用实际的例子来进行演示,以便让朋友们有一个直观的入门认识。在实际情况中,SQL注入细分也有很多的类型,而完善的处理方式也包括controller层的输入检验等,甚至api网关或者反向代理也需要考虑这样的情况,此处不再深入。
鸣谢:
https://www.geeksforgeeks.org/sql-injection/