JeecgBoot安全防护实战:SQL注入与XSS漏洞的纵深防御指南

📅 2026/6/30 18:26:46
JeecgBoot安全防护实战:SQL注入与XSS漏洞的纵深防御指南
1. 项目概述为什么JeecgBoot开发者必须关注安全防护如果你正在使用JeecgBoot进行企业级应用开发或者你的项目已经上线运行那么“安全”这个词绝对不应该只停留在项目启动会的PPT里。我见过太多基于快速开发框架搭建的系统初期为了赶进度所有精力都扑在功能实现上安全配置要么沿用框架默认要么干脆被忽略。等到项目上线被安全扫描工具扫出一堆“高危”漏洞或者更糟——真的被攻击了才手忙脚乱地回头补窟窿成本往往是预防阶段的十倍甚至百倍。JeecgBoot作为一个优秀的低代码开发平台极大地提升了我们的开发效率。但效率提升的同时也带来一个潜在的认知误区框架“封装”好了安全就“自动”解决了。事实绝非如此。框架提供了基础的安全骨架和工具但肌肉和神经——也就是具体业务逻辑中的安全细节——必须由我们开发者自己来填充和加固。SQL注入和XSS跨站脚本攻击是Web安全领域最常见、也最危险的两种漏洞它们直接威胁着你的数据核心数据库和你的用户浏览器。攻击者利用它们轻则窃取、篡改数据重则获取服务器控制权整个业务系统都可能因此瘫痪。所以这份指南的目的不是复述教科书上的安全理论而是结合我在多个JeecgBoot项目中的实战经验聚焦于SQL注入和XSS这两大“顽疾”告诉你框架已经做了什么你还需要做什么以及具体每一步该怎么操作。我们会从风险原理讲起拆解JeecgBoot现有的防护机制然后深入到代码层看看如何查漏补缺最后分享一些我踩过的坑和排查技巧。无论你是刚接触JeecgBoot的新手还是维护老项目的资深开发这里都有你能直接用的“药方”。2. 核心威胁解析SQL注入与XSS的攻击原理与JeecgBoot的现状在动手加固之前我们必须清楚地知道敌人在哪里以及他们是如何进攻的。模糊的理解会导致无效的防御。2.1 SQL注入你的数据库正在“裸奔”吗SQL注入的本质是攻击者将恶意的SQL代码“注入”到应用程序原本要发送到数据库的查询语句中从而欺骗数据库执行非预期的操作。一个极其经典的例子假设你的登录功能有一段代码如下这是反面教材String sql SELECT * FROM sys_user WHERE username username AND password password ;如果用户输入的username是admin --那么拼接后的SQL会变成SELECT * FROM sys_user WHERE username admin -- AND password xxx在SQL中--是注释符这意味着后面的密码检查条件被完全注释掉了攻击者只用输入一个已知的用户名和这个简单的注入payload就能直接以该用户身份登录。更危险的攻击还包括利用UNION查询窃取其他表数据、使用SELECT INTO OUTFILE写入Webshell、甚至通过堆叠查询执行系统命令。在搜索框、订单筛选、数据ID查询等任何拼接SQL的地方都可能存在注入点。JeecgBoot的默认防护JeecgBoot基于MyBatis-Plus这是一道重要的防线。MyBatis-Plus强烈推荐并使用#{}预编译占位符。当使用#{}时MyBatis会将参数转换为一个预编译语句PreparedStatement的参数数据库会先将SQL语句的骨架编译好然后将用户输入的数据纯粹当作“数据”来处理而不是SQL代码的一部分。这样无论用户输入什么都无法改变原SQL语句的结构。!-- 安全的方式 -- select idselectUser resultTypeUser SELECT * FROM sys_user WHERE username #{username} /select然而危险潜藏在细节中${}的误用在动态排序、表名等不得已的场景下会使用${}进行字符串替换。如果这里的值来自用户输入且未过滤注入漏洞就产生了。XML中的模糊查询LIKE %${keyword}%这种写法是高风险行为。注解SQL中的拼接在Select等注解中直接拼接SQL字符串风险极高。非MyBatis操作极少数情况下如果项目中有原始的JDBC操作或JdbcTemplate执行拼接SQL那就是完全无防护的状态。注意不要迷信MyBatis-Plus就等于绝对安全。代码审计时要像侦探一样搜寻项目中所有使用${}和SQL字符串拼接的地方。2.2 XSS攻击当你的页面成了攻击者的“扩音器”XSS攻击的原理与SQL注入类似不过是发生在浏览器端。攻击者将恶意脚本代码“注入”到网页中当其他用户浏览该页面时嵌入的脚本就会被执行。XSS主要分为三类反射型XSS恶意脚本来自当前HTTP请求。比如一个搜索功能将搜索关键词原样显示在结果页面上p您搜索的关键词是${keyword}/p。如果关键词是scriptalert(xss)/script脚本就会执行。这种攻击通常需要诱骗用户点击一个构造好的链接。存储型XSS恶意脚本被永久存储到服务器数据库、文件等当任何用户访问到展示该数据的页面时脚本都会执行。比如论坛的帖子内容、用户昵称、评论信息未经过滤就存入数据库并展示危害最大。DOM型XSS前端JavaScript代码不当地操作DOM将不可信的数据当作HTML或脚本插入页面。其恶意数据不经过服务器在客户端直接触发。一个存储型XSS的简单例子用户提交的昵称设置为img src1 onerroralert(你的cookie是document.cookie)。 如果后端未过滤前端未转义就直接将昵称输出为HTML那么任何看到该用户昵称的人其会话Cookie都可能被此脚本窃取并发送到攻击者的服务器。JeecgBoot的默认防护JeecgBoot前端通常采用Vue或React等现代化框架这些框架在默认情况下对渲染到模板中的数据进行了HTML转义。例如Vue的{{ data }}插值和React的{data}都会自动转义将变为lt;变为gt;从而防止脚本执行。这是一道强大的基础防线。但是防护缺口依然存在v-html/dangerouslySetInnerHTML当我们需要动态渲染HTML内容时会使用这些指令。如果渲染的内容来自用户输入或不可信的第三方且未经净化XSS漏洞立刻出现。富文本编辑器内容这是最大的挑战。像用户发布的文章、商品详情等本身就需要包含HTML标签如加粗、图片、链接。我们不能简单粗暴地转义所有HTML否则格式全无。这里需要的是“白名单过滤”。JavaScript内联事件和URL即使不用v-html如果将用户输入直接拼接到HTML属性如onclickalert(${data})或a href${url}的href里也可能构成XSS如hrefjavascript:alert(1)。接口响应直接使用有时前端会直接将某个接口返回的字段如错误信息、提示文本用innerHTML或类似方式插入DOM如果该接口字段被污染则前端防线失守。3. 纵深防御实战从全局配置到代码细节理解了威胁我们就要构建多层次的防御体系。安全领域讲究“纵深防御”即不依赖单一措施而是在多个层面设置关卡。3.1 第一道防线框架层与全局过滤在动手写业务代码之前先检查并加固框架提供的全局安全配置。1. 确保XSS过滤器启用JeecgBoot通常内置或推荐使用Spring Boot的防XSS过滤。检查你的pom.xml是否引入了相关依赖例如常见的antisamy或xssfilter。然后检查是否有全局的XSS过滤配置类。一个基于Jackson的全局JSON序列化XSS过滤示例Configuration public class XssConfig { Bean Primary public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); // 注册一个自定义的序列化器用于处理字符串类型的字段 SimpleModule module new SimpleModule(); module.addSerializer(String.class, new JsonHtmlXssSerializer()); mapper.registerModule(module); return mapper; } public static class JsonHtmlXssSerializer extends JsonSerializerString { Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value ! null) { // 使用工具类进行HTML转义 String escapedValue HtmlUtils.htmlEscape(value); gen.writeString(escapedValue); } } } }这个配置会对所有通过ResponseBody返回的JSON字符串字段进行HTML转义。但请注意这是一把“双刃剑”。它会转义所有字符串包括那些需要富文本的字段。因此更佳实践是全局过滤用于防御特定字段富文本单独处理。我们可以在需要保留HTML的字段上使用JsonSerialize注解指定自定义的、不过滤的序列化器。2. 配置HTTP安全头在application.yml或安全配置类中强化Spring Security或直接使用Filter来设置安全头# 在Spring Security配置中或使用专门的filter security: headers: content-security-policy: default-src self; script-src self unsafe-inline unsafe-eval https://cdn.example.com; style-src self unsafe-inline; x-content-type-options: nosniff x-frame-options: DENY x-xss-protection: 1; modeblockContent-Security-Policy (CSP)这是防御XSS的终极武器之一。它告诉浏览器只允许加载和执行来自指定来源的脚本、样式等资源。即使页面被注入了恶意脚本浏览器也会拒绝执行。配置CSP需要仔细规划你的资源来源。X-Frame-Options: 防止页面被嵌套在iframe里用于对抗点击劫持。X-XSS-Protection: 启用浏览器内置的XSS过滤虽然现代浏览器更推荐CSP。3.2 第二道防线数据访问层SQL注入防御核心这里是防御SQL注入的主战场。原则是能不用${}就不用万不得已要用必须严格校验。1. 坚持使用#{}预编译这是铁律。在MyBatis/MyBatis-Plus的XML映射文件和注解中所有来自用户输入的参数必须使用#{}。!-- 好 -- select idselectByCondition resultType... SELECT * FROM table WHERE name #{name} AND status #{status} /select !-- 危险 -- select idselectOrder resultType... SELECT * FROM table ORDER BY ${orderField} ${orderType} /select2. 安全地处理动态排序/表名对于ORDER BY或动态表名这种必须使用${}的场景必须进行白名单校验。// 在Service层或一个专门的工具类中进行校验 public String safeOrderBy(String field, String direction) { // 定义允许排序的字段白名单 SetString allowedFields new HashSet(Arrays.asList(create_time, update_time, name)); SetString allowedDirections new HashSet(Arrays.asList(ASC, DESC)); if (!allowedFields.contains(field)) { field create_time; // 提供默认值 } if (!allowedDirections.contains(direction.toUpperCase())) { direction DESC; } return field direction; } // 在XML中调用 ORDER BY ${safeOrderBy(param.orderField, param.orderType)}对于动态表名同样需要基于业务逻辑进行严格校验确保其值只能是有限的、预期的几个。3. 模糊查询的正确姿势模糊查询LIKE必须使用#{}并结合CONCAT函数或MyBatis的bind标签。!-- 方法一使用 CONCAT -- select idsearch resultType... SELECT * FROM table WHERE name LIKE CONCAT(%, #{keyword}, %) /select !-- 方法二使用 bind (更清晰) -- select idsearch resultType... bind namepattern value% keyword % / SELECT * FROM table WHERE name LIKE #{pattern} /select4. 使用MyBatis-Plus的WrapperMyBatis-Plus的QueryWrapper、LambdaQueryWrapper等其内部方法如eq,like,ge都是基于预编译实现的可以放心使用。LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(User::getName, keyword) // 安全内部使用预编译 .eq(User::getStatus, status); userMapper.selectList(wrapper);3.3 第三道防线业务逻辑层与控制器层XSS防御核心这一层负责处理用户输入和准备输出数据是控制XSS的关键。1. 输入验证与净化不要只依赖前端验证。后端必须对所有输入进行校验。格式校验使用JSR 303 Bean Validation注解如NotBlank,Email,Size。业务逻辑校验检查数值范围、状态是否合法等。针对XSS的净化对于普通的文本输入如用户名、标题可以在接收参数时进行HTML转义。Spring提供了HtmlUtils.htmlEscape()方法。但更好的做法是在输出时转义保持数据在系统中的原始性。对于富文本则需要专门的净化库。2. 富文本内容的处理重中之重这是XSS防御最复杂的一环。你需要引入一个HTML净化库如JSoup或OWASP Java HTML Sanitizer。使用JSoup进行白名单过滤的示例import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; public class HtmlSanitizer { // 定义一个相对宽松但安全的富文本白名单 private static final Safelist CONTENT_SAFELIST Safelist.relaxed() .addTags(div, p, br, hr, h1, h2, h3, h4, h5, h6) .addAttributes(a, href, title, target) // 允许链接的href和target属性 .addProtocols(a, href, http, https, mailto) // 限制链接协议 .addAttributes(img, src, alt, title, width, height) .addProtocols(img, src, http, https) .addAttributes(:all, style) // 谨慎允许style .addAttributes(table, class, border, cellpadding, cellspacing) .addAttributes(td, colspan, rowspan); /** * 净化富文本HTML * param dirtyHtml 用户提交的原始HTML * return 净化后的安全HTML */ public static String sanitizeRichText(String dirtyHtml) { if (dirtyHtml null || dirtyHtml.trim().isEmpty()) { return ; } // 使用JSoup进行净化 String cleanHtml Jsoup.clean(dirtyHtml, CONTENT_SAFELIST); // 可选进一步处理如相对路径转绝对路径等 return cleanHtml; } /** * 净化纯文本用于非富文本字段如标题、摘要 * param input 用户输入 * return 纯文本所有HTML标签被转义或移除 */ public static String sanitizePlainText(String input) { if (input null) { return null; } // 使用Safelist.none()移除所有标签只保留文本 return Jsoup.clean(input, Safelist.none()); } }在Service层或Controller层在处理保存富文本内容如文章详情之前调用sanitizeRichText方法。对于标题、摘要等调用sanitizePlainText。3. 输出转义这是最后一道也是确保万无一失的防线。即使数据在入库时被污染在展示时转义也能防止脚本执行。前端框架默认转义信任Vue/React的默认插值行为。避免危险的指令严格审查代码中使用v-html或dangerouslySetInnerHTML的地方确保其内容要么是完全可信的如来自后端已净化的富文本要么在渲染前进行了客户端净化。URL和属性值处理对于动态生成的链接href或属性src使用前端方法进行校验。例如href确保以http://、https://或mailto:开头避免javascript:协议。3.4 第四道防线依赖与运维安全1. 定期更新依赖使用Maven或Gradle的dependency:check或dependencyCheck插件定期扫描项目依赖的已知漏洞CVE。JeecgBoot本身、Spring Boot、MyBatis-Plus以及你引入的其他第三方库都需要保持最新稳定版本。2. 数据库最小权限原则为应用数据库连接账户分配最小必要的权限。通常业务应用只需要SELECT,INSERT,UPDATE,DELETE权限绝对不要授予DROP,CREATE,ALTER,FILE,PROCESS等高级权限。这样即使发生SQL注入攻击者能造成的破坏也有限。3. 部署WAFWeb应用防火墙在应用服务器前部署WAF可以作为一道有效的通用防护层拦截常见的攻击模式。但记住WAF是补充不能替代代码层面的安全。4. 实战排查与常见问题解决即使做了以上所有防护在开发、测试和上线后我们仍需保持警惕。以下是我在实践中总结的排查清单和问题解决方法。4.1 代码审计自查清单定期或在新功能上线前对代码进行安全审计。你可以围绕以下问题清单进行SQL注入方面[ ] 项目中是否还有直接使用字符串拼接的SQL全局搜索拼接或[ ] 所有MyBatis XML中的${}使用场景是否都经过审核对应的参数是否做了白名单校验[ ] 注解SQL如Select中是否有拼接[ ] 是否使用了EntityWrapper的where方法拼接字符串旧版本风险[ ] 是否有绕过MyBatis直接使用JdbcTemplate或JDBC的地方XSS方面[ ] 前端项目中全局搜索v-html或dangerouslySetInnerHTML检查其数据来源。[ ] 检查所有将变量插入href、src、onclick等属性的地方。[ ] 后端接口返回的数据哪些字段是富文本它们是否有专门的净化处理[ ] 错误信息、提示信息是否直接返回给前端并显示是否可能包含用户输入4.2 常见问题与解决方案问题1使用了#{}但日志里显示SQL语句还是被“拼接”了是不是有注入风险解答这是常见的误解。MyBatis日志中打印的 Parameters: xxx和 Preparing: SELECT ... WHERE id ?其中的?就是预编译占位符。后面打印的 Parameters: 1(Integer)是将参数传给占位符。数据库驱动最终发送给数据库的是预编译的语句和参数集参数值不会改变语句结构。所以这是安全的正常日志无需担心。问题2富文本过滤太严格用户需要的样式如特定的class、背景色被过滤掉了怎么办解答这是业务需求与安全的平衡。你需要和产品经理、运营人员共同确定一个安全的HTML标签和属性白名单。使用JSoup的Safelist可以非常灵活地配置。如果用户需要更复杂的样式可以考虑引入一个安全的、受控的富文本编辑器如基于Slate.js或Quill的自定义编辑器它只输出你允许的格式。绝对不要为了功能而放开script、iframe、on*事件等危险元素和属性。问题3全局XSS过滤器把富文本字段也转义了导致页面显示HTML源码。解答这正是为什么推荐“输出时转义”或“按字段差异化处理”的原因。解决方案禁用全局过滤对特定字段的影响如果使用了全局的Jackson序列化过滤可以给富文本字段的对应DTO属性加上JsonSerialize(using RawStringSerializer.class)注解使用一个不做转义的序列化器。前后端协作后端返回原始的、已净化的富文本HTML字符串。前端在展示时在可信的容器内如一个专门用于展示富文本的div使用v-html指令渲染。关键在于这个HTML字符串在入库前必须经过严格的白名单净化。问题4动态查询条件非常复杂QueryWrapper不够用又不想用${}怎么办解答MyBatis-Plus提供了QueryWrapper的链式调用能满足绝大部分动态查询。对于极端复杂的场景可以考虑使用MyBatis的动态SQL标签if,choose,foreach这些标签内部也是使用#{}。将复杂查询拆分成多个简单的查询在Service层组合结果。实在无法避免时使用存储过程或数据库视图来封装复杂逻辑应用层只传递参数调用。但需注意存储过程本身也可能有注入风险需同样使用参数化查询。问题5如何对上线后的系统进行快速安全检测解答自动化扫描工具使用OWASP ZAP或Burp Suite的主动扫描功能对系统接口进行漏洞扫描。它们能模拟SQL注入和XSS攻击并报告潜在问题。手动测试SQL注入在所有输入框、URL参数后尝试添加、、--、#、/*观察页面是否报数据库错误如MySQL、PostgreSQL的错误信息或行为是否异常如登录绕过。XSS在文本输入处尝试scriptalert(1)/script、img srcx onerroralert(1)、svg onloadalert(1)观察弹窗是否出现。测试hrefjavascript:alert(1)。代码审计工具使用SonarQube、Fortify等静态代码分析工具集成到CI/CD流程中自动发现潜在的安全代码模式。安全是一个持续的过程而不是一次性的任务。将上述防护措施融入到你的开发规范、代码审查和上线流程中才能为你的JeecgBoot应用构建起坚固的防线。记住框架负责提供武器而如何布防取决于每一位开发者。