在后台系统里,存储过程常直接接收前端传来的参数去做增删改查。如果不在过程内部先核对参数合法性,脏数据或恶意字符串就会流进表里面。编写带输入校验的SQL存储过程,核心思路是在真正执行业务SQL之前,用正则表达式或者LIKE匹配对入参做格式检查,不通过就主动报错并结束。
为什么要在存储过程里做输入校验
很多人习惯把校验放在应用层,比如用Java或Python先判断再调数据库。但多端共用同一个库时,网页、小程序、定时脚本都可能直接调用存储过程,应用层拦不住全部入口。把校验下沉到存储过程,相当于在数据库门口设了一道统一关卡,无论谁调用都必须过检。
另一个现实问题是SQL注入。拼字符串执行虽然方便,但遇到特殊字符就危险。存储过程本身参数化执行更安全,若再配合输入格式校验,能进一步缩小被攻击面。比如要求用户名只能含字母数字,用正则一匹配,带分号或引号的统统进不来。
使用LIKE匹配实现简单校验
LIKE适合做前缀、后缀或包含关系的模糊匹配,语法简单、各数据库都支持,性能也轻。比如只允许以A开头的订单号,或者参数里不能出现某些关键词,用LIKE配合通配符就很直观。它不支持复杂规则,但应对基础格式够用。
下面以MySQL为例,写一个校验用户名只能以字母开头、且不含空格的存储过程。我们用LIKE '% %'判断是否有空格,再用LEFT取首字符看是不是字母。虽然不如正则严谨,但逻辑清楚,初学者也容易改。
DELIMITER $$
CREATE PROCEDURE add_user_if_valid (
IN p_username VARCHAR(50)
)
BEGIN
-- 若包含空格则非法
IF p_username LIKE '% %' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户名不能包含空格';
END IF;
-- 首字符不是字母则非法
IF p_username NOT REGEXP '^[A-Za-z]' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户名必须以字母开头';
END IF;
INSERT INTO users(username) VALUES (p_username);
END$$
DELIMITER ;
上面例子里其实混用了一点正则,纯粹LIKE版本可以写成判断首字符用子串函数。LIKE的优势是可读,缺点是对“只能由某几类字符组成”这种全串规则表达力弱,写多了嵌套会乱。
使用正则表达式做严格校验
MySQL、PostgreSQL、Oracle都支持REGEXP或相似正则语法,能描述整串规则。比如邮箱、手机号、身份证,用一条正则比写一堆LIKE直观得多。正则代价是稍多一点计算,但在入参长度有限时影响很小。
以下过程要求传入的手机号必须是11位数字且以1开头,否则报错。正则'^1[0-9]{10}$'直接从头到尾锁定格式,避免中途混字母。
DELIMITER $$
CREATE PROCEDURE save_phone_if_valid (
IN p_phone VARCHAR(20)
)
BEGIN
IF p_phone NOT REGEXP '^1[0-9]{10}$' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '手机号格式不正确';
END IF;
INSERT INTO contact(phone) VALUES (p_phone);
END$$
DELIMITER ;
正则写错了容易让合法数据被拒,所以规则要确定再上线。建议把正则串提取成过程顶部变量或注释写明含义,方便后续维护。PostgreSQL里用的是~操作符,Oracle可用REGEXP_LIKE函数,思路一致。
正则与LIKE怎么选
从场景看,LIKE适合模糊、简单、可读性优先的校验;正则适合强格式、精确全串匹配。若表量大且过程被高频调用,LIKE通常更省CPU。但像邮箱这种规则,硬用LIKE要拆好几段,反而更难懂。
| 对比项 | LIKE | 正则表达式 |
|---|---|---|
| 语法复杂度 | 低 | 中高 |
| 表达全串规则 | 弱 | 强 |
| 数据库支持 | 全部 | 主流均支持 |
| 性能开销 | 较小 | 略大 |
实际项目常组合使用:先用LIKE挡掉明显异常,再用正则做精细核对。这样既不牺牲太多性能,也保住校验强度。
完整可运行示例
下面给一个综合例子,过程接收邮箱与年龄,邮箱用正则、年龄用范围加LIKE无关判断,展示如何在一个过程里组织多条校验并给出明确错误。
DELIMITER $$
CREATE PROCEDURE register_user (
IN p_email VARCHAR(100),
IN p_age INT
)
BEGIN
IF p_email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '邮箱格式不合法';
END IF;
IF p_age < 1 OR p_age > 150 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '年龄超出合理范围';
END IF;
INSERT INTO account(email, age) VALUES (p_email, p_age);
END$$
DELIMITER ;
调用时若传错邮箱会直接收到报错,不会插进表。注意代码里把小于号写成<、大于号写成>,避免被当标签解析。这种写法在各类SQL教程里都算规范做法。
常见误区与注意点
有人喜欢在过程里用SELECT拼字符串再PREPARE EXECUTE,这时校验再严也要防引号逃逸。本文示例都用参数化INSERT,天然隔离。另外 SIGNAL 抛错要在支持的事务上下文里,若外层没捕获,调用端会收到异常,属正常表现。
还有一点,正则在不同数据库里元字符略有差异,比如MySQL用双反斜杠表示转义,PostgreSQL单斜杠即可。写跨库脚本时要分别测试,不要假定一套正则通吃。先把输入校验写稳,再谈业务逻辑的复杂度。