真正有效的测试必须触发业务层异常而非数据库异常,需走真实dao路径、覆盖${}拼接等高危分支,并验证二次注入全链路。

真正有效的测试必须让恶意输入穿透校验层、拼接逻辑和 DAO 调用,最终触发业务层异常(如 IllegalArgumentException),而不是只验证“没报错”或 mock 返回值。
测试必须走真实 DAO 调用路径,不能 mock 查询方法
很多测试失败不是因为防护没写对,而是测试本身绕过了关键路径。比如在 service 层写 when(userRepository.findByUsername(any())).thenReturn(...),等于把恶意字符串“admin'--”根本没送进方法体——你测的是 mock 配置,不是防护逻辑。
- 正确做法:调用
userService.findByUsername("admin'--"),确保输入流经参数校验 → SQL 构造 →JdbcTemplate或MyBatis执行 - 若底层是 MyBatis,确认测试命中的是
#{name}路径,而非${name}拼接分支;后者必须单独构造用例验证 - 对 JSON 接口,测试原始 body 含
%27(URL 编码单引号),断言解码后仍被拦截,而非仅测前端过滤
断言目标必须是业务层异常,不是数据库异常
看到测试通过就以为安全?错。如果实际抛出 PSQLException 或 MySQLSyntaxErrorException,说明请求已抵达数据库,防护彻底失效。
- ✅ 正确断言:
assertThatThrownBy(() -> service.login("root' OR '1'='1")).isInstanceOf(IllegalArgumentException.class) - ❌ 错误断言:
assertThatCode(() -> service.login("")).doesNotThrowAnyException()—— 这只说明没崩,不等于没进 DB - 若使用自定义异常(如
InvalidInputException),断言必须精确匹配该类型,不能宽泛 catchRuntimeException
必须覆盖 ${} 拼接、String.format、+ 拼接等高危分支
参数化查询(#{})救不了所有场景。旧代码里藏得深的动态表名、字段名拼接,或先查模板再填充的逻辑,都是盲区。
- 对 MyBatis 的
${tableName},传入"users; DROP TABLE admins--",验证是否抛出预期异常 - 对
String sql = "SELECT * FROM " + userInput;类代码,测试需触发运行时异常(如SQLException)或记录非法日志(检查 logback-test.xml 中是否捕获"illegal dynamic table name") - 对 JPA
createNativeQuery,必须显式构造含单引号的参数,验证是否被拦截而非交由驱动处理
最容易被忽略的点是:二次注入——恶意数据先存入 DB,后续查询时再拼接使用。这类场景的测试必须模拟“存→查→拼→执行”全链路,且中间不能做任何额外转义。否则你以为拦住了输入,其实只是把炸弹延后引爆了。











