-
前端过滤,输入框中过滤特殊字符,或者限制输入的字符集合
-
代码层手动过滤,同上
-
利用连接池组件过滤,比如druid的WallFilter
-
使用预编译,比如 jdbc 的preparedStatement
-
使用存储过程,只传入参数
这里详细说下mysql jdbc防止sql注入的原理
1.代码层手动过滤
首先看下com.mysql.jdbc.PreparedStatement的setString 源码,可以看到setString对参数做了一定的处理,比如增加单引号,对换行符等一些字符做了转义处理,也就是说mysql jdbc 封装了一些代码层手动过滤,
经过setString 方法对输入字符串参数进行处理后,消除了一部分sql注入的威胁
public void setString(int parameterIndex, String x) throws SQLException {
synchronized(this.checkClosed().getConnectionMutex()) {
if (x == null) {
this.setNull(parameterIndex, 1);
} else {
this.checkClosed();
int stringLength = x.length();
StringBuffer buf;
if (this.connection.isNoBackslashEscapesSet()) {
boolean needsHexEscape = this.isEscapeNeededForString(x, stringLength);
Object parameterAsBytes;
byte[] parameterAsBytes;
if (!needsHexEscape) {
parameterAsBytes = null;
buf = new StringBuffer(x.length() + 2);
buf.append('\'');
buf.append(x);
buf.append('\'');
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(buf.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(buf.toString());
}
this.setInternal(parameterIndex, parameterAsBytes);
} else {
parameterAsBytes = null;
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(x);
}
this.setBytes(parameterIndex, parameterAsBytes);
}
return;
}
String parameterAsString = x;
boolean needsQuoted = true;
if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) {
needsQuoted = false;
buf = new StringBuffer((int)((double)x.length() * 1.1D));
buf.append('\'');
for(int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch(c) {
case '\u0000':
buf.append('\\');
buf.append('0');
break;
case '\n':
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\u001a':
buf.append('\\');
buf.append('Z');
break;
case '"':
if (this.usingAnsiMode) {
buf.append('\\');
}
buf.append('"');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '¥':
case '₩':
if (this.charsetEncoder != null) {
CharBuffer cbuf = CharBuffer.allocate(1);
ByteBuffer bbuf = ByteBuffer.allocate(1);
cbuf.put(c);
cbuf.position(0);
this.charsetEncoder.encode(cbuf, bbuf, true);
if (bbuf.get(0) == 92) {
buf.append('\\');
}
}
default:
buf.append(c);
}
}
buf.append('\'');
parameterAsString = buf.toString();
}
buf = null;
byte[] parameterAsBytes;
if (!this.isLoadDataQuery) {
if (needsQuoted) {
parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
}
} else {
parameterAsBytes = StringUtils.getBytes(parameterAsString);
}
this.setInternal(parameterIndex, parameterAsBytes);
this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 12;
}
}
}
2.预编译功能
预编译的功能是在服务端开启的,mysql预编译功能默认是关闭的,通过对jdbc url 追加以下参数来开启 useServerPrepStmts=true&cachePrepStmts=true
参数名称 | 说明 |
---|---|
useServerPrepStmts | 开启预编译功能,开启后客户端会发送prepare语句到l服务端 |
cachePrepStmts | 服务端缓存预编译结果,不开启的话,即使相同的sql每次都需要重新预编译,开启后有助于性能提升 |
开启上述两个功能之后,服务端预编译完成后会存储预编译结果,并且将预编译结果对应的key回传到jdbc客户端,jdbc客户端将key进行缓存,使用时发现相同的sql,就会直接使用缓存的key,将key和参数发送到服务端,从而实现相同的sql不需要编译多次
开启预编译以后,含有占位符的sql会被先发送到服务端,进行语法,词法等分析,生成预编译结果,预编译结果就类似方法一样,方法内部的查询哪张表,使用哪个字段查询,这些逻辑已经确定了,只需要传入响应的参数即可,这个预编译结果就是防止注入的关键所在,由于已经经过了语法词法分析,sql的执行路径已经完全确定,当含有注入风险的参数传入时,参数只当作数据来处理,不会对参数进行语法词法分析,也就不能影响sql的执行路径,因此能够完全的防止sql的注入