开始
1.痛点分析
项目中需要进行超多条件筛选,示例页面如下:
这里检索条件非常多,且是不能写死的,需要灵活多变,以应对甲方需求的灵(改)活(你)多(大)变(爷)。
2.采用框架
后台 mybatis / mysql
前端 Layui / Jquery
3.mybatis是如何防止sql注入攻击的
SQL注入攻击举例:在你的input中输入‘ 0 or id= 123 ’,则很有可能你的后台会执行 where gender = 0 or id = 123 这只是很小的例子,一般前端要做表单值验证(一般用正则表达式过滤),如果前后端都不设防,系统将有很大风险。
先贴一段mybatis的sql片段
<select id="select" resultMap="BaseResultMap" parameterType="com.core.entity.User" >
select
*
from sys_user
<where>
<if test="id != null">
and id = #{id,VARCHAR}
</if>
<if test="age != null">
and age = #{age,INTEGER}
</if>
<if test="note != null">
${note}
</if>
</where>
order by crttime
limit ${m},${n}
</select>
可以看到,以上SQL标签内有三个条件标签。
其中#{id,VARCHAR}以及 #{age,INTEGER}在执行时(预编译),会对输入字段进行检查并且格式化输出内容。
比如 id = #{123,VARCHAR} 输出 id = ‘123’。
而 age = #{12,INTEGER} 输出 age = 12。
也就是字符串格式会自动包含引号,而数字不会。
通俗说,
#{属性值,属性类型} 格式,是防sql注入的。
${属性值} 是直接输入,不检查和格式化的,不防sql注入。
所以,在mybatis中,使用 ${属性值} 的这种写法,是不安全的。
在实际开发中,前端是要对用户输入的字段进行格式检查的,再加上mybatis的格式检查,很大程度上防范了sql注入攻击。
4.实现
4.1 以对象传参形式将sql条件语句转换为对象。
其实解决多条件筛选最简单暴力的方法,就是在前端拼接sql,然后将sql传到后台执行查询。
历史:2010年以前的很多JAVA老项目,jsp内使用el表达式写满sql语句,密密麻麻程度令人发狂,例如:
这样的方式虽然简单,但却将sql句子暴露给所有人看,你的数据结构将暴露无遗,且面临sql注入攻击的风险。
为了不重蹈覆辙,这里我自写内部解析方法,进行安全的sql片段还原,这是解决多条件筛选的一个思路。
/*
*前台js代码*
*/
var _condition = [];//构造sql拼接对象
_condition.push({keywords: 'like', column: 'phone', value: '188'});//意为phone like '%188%'
_condition.push({keywords: 'in', column: 'type', values: ['normal','pause']});//意为 type in ('normal','pause')
//以下简写,请求数据,传输sql拼接对象
ajax({
...
url:'**/getPageData',
data:{
c: JSON.stringify(_condition), //将对象以json字符串形式传输,也可以考虑序列化
m:0, //分页参数
n:10 //分页参数
},
success:function(e){
//获取结果集
}
});
4.2 后台接收对象并解析成sql查询语句
//接收到前端传来请求,将json字符串转化对象并还原成sql语句
...处理接收到的数据c,其他省略...
String sql = parseCondtionSql(c);
//后台解析的方法
private String parseCondtionSql(String sqlObjectJson){
if(isEmptyStr(sqlObjectJson)){
return null;
}
StringBuilder sb = new StringBuilder();
List<Map<String,Object>> queryList = (List<Map<String,Object>>) super.jsonToObject(sqlObjectJson, List.class);
for(Map<String,Object> single : queryList){
if("null".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" is null ");
}
if("notnull".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" is not null ");
}
if("eq".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" = ")
.append("'")
.append(sqlFormater(single.get("value").toString()))
.append("' ");
}
if("numeq".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" = ")
.append(sqlFormater(single.get("value").toString()))
.append(" ");
}
if("neq".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" != ")
.append("'")
.append(sqlFormater(single.get("value").toString()))
.append("' ");
}
if("like".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" like ")
.append(" '%")
.append(sqlFormater(single.get("value").toString()))
.append("%' ");
}
if("likes".equals(single.get("keywords"))){
sb.append(" and ( ");
List<String> column = (List) single.get("column");
int index = 0;
for(String c : column){
if(index != 0){
sb.append(" or ");
}
sb.append(sqlFormater(c))
.append(" like ")
.append(" '%")
.append(sqlFormater(single.get("value").toString()))
.append("%' ");
index ++;
}
sb.append(" ) ");
}
if("likeFront".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" like ")
.append(" '")
.append(sqlFormater(single.get("value").toString()))
.append("%' ");
}
if("likeBehind".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" like ")
.append(" '%")
.append(sqlFormater(single.get("value").toString()))
.append("' ");
}
if("in".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" in ( ");
List<String> values = (List) single.get("values");
int index = 0;
for(String v : values){
if(index != 0){
sb.append(" , ");
}
sb.append(" '")
.append(sqlFormater(v))
.append("' ");
}
sb.append(" ) ");
}
if("range".equals(single.get("keywords"))){
Map<String,Object> vObj = (Map<String, Object>) single.get("value");
if(!isEmptyStr((String)vObj.get("start"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" >= '")
.append(sqlFormater(vObj.get("start").toString()))
.append(" 00:00:00' ");
}
if(!isEmptyStr((String)vObj.get("end"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" < '")
.append(sqlFormater(vObj.get("end").toString()))
.append(" 23:59:59' ");
}
}
if("morethan".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" >= ")
.append(sqlFormater(single.get("value").toString()));
}
if("lassthan".equals(single.get("keywords"))){
sb.append(" and ")
.append(sqlFormater(single.get("column").toString()))
.append(" <= ")
.append(sqlFormater(single.get("value").toString()));
}
}
return sb.toString();
}
//后台尽可能防sql注入,将字段名和字段值,都去除关键符号并去除空格。
private String sqlFormater(String value) {
return value.replaceAll("'", "")
.replaceAll(",", "")
.replaceAll("=", "")
.replaceAll(" ", "");
}
得到转换sql片段:
" and phone like '%188%' and type in ('normal','pause') "
4.3 将sql片段插入到mapper.xml中
<select id="selectPage" resultMap="BaseResultMap" parameterType="com.core.entity.User" >
select
*
from sys_user
<where>
<if test="sql != null">
${sql} <!-- 此处是还原的sql片段 -->
</if>
</where>
order by crttime
limit ${m},${n}
</select>
最终执行sql:
select
*
from sys_user
where
1 = 1
and phone like '%188%' and type in ('normal','pause')
order by crttime
limit 0,10
其实原理so简单,没有什么绚丽的逻辑,就是最笨的方法。
前端想以什么条件做筛选都可以,然后组拼对象,对sql片段加以格式限制,从而在保证数据结构不被窥探的同时最大限度防止SQL注入攻击。
-end
有任何错误欢迎指正,有更好方法也请提点,谢谢。