在涉及数据库交互的应用开发中,SQL输入有效性校验和提交频率限制是两项核心防护手段。前者能避免恶意输入引发的SQL注入风险,后者可防止高频无效请求拖垮数据库,两者结合能大幅提升系统的安全性和稳定性。

SQL输入有效性校验方法
SQL输入校验的核心目标是确保用户输入的内容符合预期格式,且不会破坏SQL语句的结构。常见的校验方式有以下几种:
1. 参数类型与格式校验
首先对输入参数的类型、长度、格式进行基础校验,比如预期是整数的参数,要过滤掉非数字字符,避免传入异常值。
// Java示例:校验输入是否为合法整数
public boolean validateIntegerInput(String input) {
if (input == null || input.trim().isEmpty()) {
return false;
}
try {
Integer.parseInt(input.trim());
return true;
} catch (NumberFormatException e) {
return false;
}
}
2. 特殊字符过滤与转义
对输入中的SQL特殊字符进行过滤或转义,比如单引号、分号、注释符等,避免输入内容被拼接进SQL语句后改变原有逻辑。注意转义要适配使用的数据库类型,不同数据库的转义规则可能存在差异。
3. 使用预编译语句
预编译语句(如JDBC的PreparedStatement)会将SQL语句和参数分开处理,参数会被当作纯数据处理,不会参与SQL语句的解析,从根源上避免SQL注入问题,是推荐的输入处理方式。
// Java预编译语句示例,避免SQL注入
public User queryUserById(String userId) {
// 先校验输入合法性
if (!validateIntegerInput(userId)) {
return null;
}
String sql = "SELECT * FROM user WHERE id = ?";
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, Integer.parseInt(userId));
ResultSet rs = ps.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
return user;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
令牌桶算法限制提交频率
令牌桶算法是一种常用的流量控制算法,核心逻辑是系统以固定速率向桶中添加令牌,每次请求需要先获取令牌才能执行,若桶中无令牌则拒绝请求,以此限制单位时间内的请求数量。
算法核心参数
- 桶容量:桶中最多能存储的令牌数量,允许短时间内的突发请求
- 令牌生成速率:单位时间内向桶中添加的令牌数量,决定长期的平均请求频率
- 当前令牌数:桶中当前剩余的令牌数量
实现示例
以下是一个简单的令牌桶算法实现,用于限制SQL提交的频率:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class TokenBucket {
// 桶容量
private final int capacity;
// 令牌生成速率,每秒生成的令牌数
private final int rate;
// 当前令牌数
private AtomicInteger tokens;
// 上次令牌更新时间
private long lastUpdateTime;
private final ReentrantLock lock = new ReentrantLock();
public TokenBucket(int capacity, int rate) {
this.capacity = capacity;
this.rate = rate;
this.tokens = new AtomicInteger(capacity);
this.lastUpdateTime = System.currentTimeMillis();
}
// 尝试获取令牌,获取成功返回true,失败返回false
public boolean tryAcquire() {
lock.lock();
try {
// 计算当前时间应该新增的令牌数
long now = System.currentTimeMillis();
long timePassed = now - lastUpdateTime;
// 时间差转换为秒,计算新增令牌
int newTokens = (int) (timePassed / 1000 * rate);
if (newTokens > 0) {
int current = tokens.get();
int updated = Math.min(current + newTokens, capacity);
tokens.set(updated);
lastUpdateTime = now;
}
// 尝试获取令牌
if (tokens.get() > 0) {
tokens.decrementAndGet();
return true;
}
return false;
} finally {
lock.unlock();
}
}
}
两者结合实现双重防护
在实际业务中,可以将SQL输入校验和令牌桶频率限制结合使用,先校验输入合法性,再通过令牌桶判断是否允许提交,流程如下:
public class SqlSubmitService {
// 初始化令牌桶,容量10,每秒生成2个令牌,即每秒最多允许2次提交,突发最多10次
private final TokenBucket tokenBucket = new TokenBucket(10, 2);
public Result submitSql(String sqlInput) {
// 第一步:校验SQL输入有效性
if (!validateSqlInput(sqlInput)) {
return Result.fail("输入内容不合法");
}
// 第二步:令牌桶校验提交频率
if (!tokenBucket.tryAcquire()) {
return Result.fail("提交频率过高,请稍后再试");
}
// 执行SQL操作
return executeSql(sqlInput);
}
// SQL输入校验逻辑
private boolean validateSqlInput(String input) {
if (input == null || input.trim().isEmpty()) {
return false;
}
// 简单校验:不允许包含常见SQL注入关键字,实际可根据需求扩展
String lowerInput = input.toLowerCase();
if (lowerInput.contains("select *") || lowerInput.contains("drop table") || lowerInput.contains("delete from")) {
return false;
}
return true;
}
private Result executeSql(String sql) {
// 实际SQL执行逻辑,建议使用预编译语句
return Result.success("执行成功");
}
// 内部结果类
static class Result {
private boolean success;
private String message;
private Result(boolean success, String message) {
this.success = success;
this.message = message;
}
public static Result success(String msg) {
return new Result(true, msg);
}
public static Result fail(String msg) {
return new Result(false, msg);
}
}
}
注意事项
1. SQL输入校验的规则需要根据业务场景动态调整,避免过度校验影响正常业务使用。
2. 令牌桶的参数(容量、生成速率)需要结合数据库的承载能力设置,避免限制过松起不到防护作用,或过严影响正常用户使用。
3. 令牌桶实现如果是分布式场景,需要使用分布式缓存(如Redis)存储令牌状态,避免多实例下限制失效。
4. 预编译语句虽然能防SQL注入,但如果是动态表名、列名等无法参数化的场景,仍需要做好输入白名单校验,只允许传入预期的内容。