spring security 本身不防 sql 注入,因其默认 filter 不解析请求内容;须自定义 filter 在 dispatcherservlet 前拦截,分层防护:过滤器做关键字初筛,controller 用 @valid 校验,dao 层禁用拼接、mybatis 用 #{}, 动态 sql 需白名单。

Spring Security 本身不防 SQL 注入,加再多 http.addFilterBefore() 也拦不住——除非你写的过滤器真正在 DispatcherServlet 前读取并校验原始请求内容。
为什么 UsernamePasswordAuthenticationFilter 之前加 filter 还是无效?
因为很多开发者误以为“插在认证过滤器前 = 拦在所有业务逻辑前”,但实际漏掉了关键前提:请求体(尤其是 application/json)只能被读取一次。如果 Spring MVC 的 ContentCachingRequestWrapper 或参数解析器已提前消费了 InputStream,你的自定义 filter 再调用 request.getInputStream() 就会抛 IllegalStateException。
- 必须用
HttpServletRequestWrapper包装原始 request,缓存 body 并重写getInputStream()和getReader() - 不能只依赖
getParameterMap()——它对 POST JSON、PUT raw body 完全不可见 - 注册时用
FilterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE),而非仅靠addFilterBefore()顺序 - 检查
Content-Type:是application/json就解析 JSON;是application/x-www-form-urlencoded就解码 query string;否则跳过 body 检查
SqlInjectFilter 怎么安全读取并检测 JSON 请求体?
直接 new ObjectMapper().readValue(request.getInputStream(), Map.class) 是错的——这会真正消耗流,后续 Controller 拿不到数据。正确做法是先缓存字节流,再双路解析:一次给过滤器做关键字扫描,一次留给 Spring MVC。
- 继承
HttpServletRequestWrapper,在构造时用IOUtils.toByteArray(request.getInputStream())一次性读完并保存为byte[] - 重写
getInputStream()返回new ByteArrayInputStream(cachedBytes) - 检测时:若
contentType含json,用new String(cachedBytes, StandardCharsets.UTF_8)得到原始字符串,再用正则匹配select|union|exec|xp_等(注意大小写不敏感) - 避免用
JSON.parseObject()全量解析——性能差,且绕过检测(如恶意字段名里藏关键字)
关键字匹配为什么不能只靠正则?哪些场景会漏掉?
正则能拦住 ?q=1' OR '1'='1,但拦不住 %55%4e%49%4f%4e%20%53%45%4c%45%43%54(UNION SELECT URL 编码)或 SEL/**/ECT(注释分隔)。纯模式匹配注定有盲区。
- 必须配合多层防护:过滤器只做“快筛”(block 明显攻击),不替代业务层校验
- Controller 层强制
@Valid @RequestBody,字段加@Pattern(regexp = "^[a-zA-Z0-9_\-]+$")等白名单约束 - DAO 层彻底禁用字符串拼接:
"WHERE name = '" + name + "'"是硬性红线,MyBatis 必须用#{name},JDBC 必须用PreparedStatement - 关键字列表别硬编码,从配置中心加载:
@Value("${security.sql-inject.keywords:select|union|drop|;|--|/\*|\*/}")
如何避免“已加 filter”却仍被攻破的幻觉?
最危险的不是没加,而是加了但没生效——比如 filter 被注册在 DispatcherServlet 之后,或只扫 GET 参数却忽略 POST body,又或者日志里全是 IllegalStateException: getInputStream() called twice 却没人看。
- 上线前必做验证:发一个含
UNION SELECT的 JSON POST 请求,抓包确认响应是 403 且日志有拦截记录 - 在 filter 的
doFilter()开头加if (request instanceof ContentCachingRequestWrapper) { log.warn("SqlInjectFilter skipped: request already wrapped"); return; },防止重复包装 - 放行路径要显式声明,别用
urlPatterns = "/*"然后靠 if 判断跳过——容易漏掉/actuator/health这类管理端点 - 不要把 SQL 防护和 XSS 混在一个 filter 里:职责分离,各自独立开关、独立日志、独立规则更新
真正起作用的 SQL 注入防护,从来不是某个 filter 的一锤定音,而是从网关层(如 Spring Cloud Gateway 的 GlobalFilter)、Web 层(HttpServletRequestWrapper)、到 DAO 层(预编译+白名单)的三道实线。任何一层松动,都可能让攻击者找到缝隙。











