1. 项目概述为什么JeecgBoot项目必须重视安全防护在快速迭代的业务开发中我们常常被功能实现和交付期限追着跑安全防护很容易被放到“上线后再优化”的清单里。但作为一名经历过多次安全审计和线上事故的老兵我必须说对于像JeecgBoot这样基于Spring Boot的快速开发平台安全不是可选项而是项目启动时就该打下的地基。尤其是SQL注入和XSS攻击这两者堪称Web应用的“头号公敌”它们利用的是应用对用户输入数据的信任一旦得手轻则数据泄露重则服务瘫痪、数据被篡改甚至服务器沦陷。JeecgBoot本身提供了强大的代码生成和基础架构能力极大地提升了开发效率。但“快”也意味着如果开发者安全意识不足那些自动生成的CRUD接口、开放的API端点都可能成为攻击者眼中的“便捷入口”。我见过太多案例一个简单的列表查询接口因为前端传入的排序字段名没有做过滤就被利用来进行SQL注入导致整张用户表被拖走。也见过一个富文本评论功能因为输出时没有转义被植入了恶意脚本导致所有访问该页面的用户Cookie被盗。因此这份指南的目的不是复述那些枯燥的安全理论而是结合JeecgBoot的技术栈Spring Boot, Mybatis-Plus, Shiro等分享一套从编码习惯、框架配置到安全工具落地的、可立即实施的防护方案。无论你是刚接手一个JeecgBoot项目还是正在基于它开发新系统这些内容都能帮你筑起第一道也是最重要的一道防线。2. 安全威胁深度解析SQL注入与XSS的攻击原理与危害在动手部署防护措施之前我们必须先理解对手。只有透彻了解攻击是如何发生的我们才能精准地设置防御点。2.1 SQL注入数据库的“万能钥匙”SQL注入的本质是攻击者将恶意的SQL代码“注入”到应用原本要发送给数据库的查询语句中从而欺骗数据库执行非预期的操作。攻击原理 想象一下JeecgBoot中一个典型的用户查询场景根据用户名查询用户详情。后端代码可能会这样写这是一个反面教材GetMapping(/user) public ListUser getUserByName(RequestParam String name) { String sql SELECT * FROM sys_user WHERE username name ; // 执行查询... return userList; }如果用户老实地输入admin那么SQL语句是正常的SELECT * FROM sys_user WHERE username admin。 但如果攻击者输入的是admin OR 11拼接后的SQL就变成了SELECT * FROM sys_user WHERE username admin OR 11这个WHERE条件永远为真导致查询返回sys_user表中的所有数据造成全表数据泄露。这还只是最简单的例子。利用SQL注入攻击者可以越权查询数据如上述例子获取所有用户信息。篡改数据通过注入UPDATE或DELETE语句修改或删除数据。例如输入admin; DELETE FROM sys_user; ----在SQL中表示注释会注释掉原语句后面的部分最终执行删除整个用户表的操作。执行管理操作某些数据库支持多语句执行攻击者可能添加DROP TABLE、SHUTDOWN等破坏性命令。窃取数据库结构利用UNION查询结合information_schema等系统表一步步摸清数据库的所有表名、字段名。服务器文件读写利用如MySQL的LOAD_FILE()和INTO OUTFILE函数读取服务器敏感文件如/etc/passwd或写入Webshell从而控制服务器。在JeecgBoot中的常见风险点MyBatis/MyBatis-Plus的${}误用在XML映射文件中使用${column}进行动态字段排序或表名拼接时如果参数来自用户输入且未过滤极易引发注入。自定义SQL拼接在Service层或工具类中手动拼接QueryWrapper的SQL片段。模糊查询参数使用LIKE查询时对参数中的通配符%,_未做处理可能导致查询结果超出预期。2.2 XSS攻击用户浏览器的“傀儡师”XSS跨站脚本攻击与SQL注入不同它的战场在用户的浏览器。攻击者将恶意脚本代码注入到网页中当其他用户浏览该页面时嵌入的脚本会被执行。攻击原理与分类反射型XSS恶意脚本作为请求参数的一部分服务器直接将其“反射”回响应页面中执行。场景一个搜索功能搜索关键词会显示在结果页面上。如果搜索接口是/search?keywordscriptalert(xss)/script且后端直接将keyword输出到HTML中那么所有看到这个结果页的用户都会弹窗。特点一次性的需要诱骗用户点击特定链接。存储型XSS恶意脚本被持久化保存到服务器数据库如评论、文章内容、用户昵称当其他用户浏览包含该数据的页面时触发。场景一个论坛的评论框。攻击者提交一条包含script窃取Cookie的代码/script的评论。此后任何用户查看这条评论时脚本都会在其浏览器中执行将用户的Cookie发送到攻击者控制的服务器。特点危害最大持久性强影响所有访问者。DOM型XSS漏洞存在于前端JavaScript代码中恶意数据在浏览器端被不安全的DOM操作所执行。场景前端JS从URL的hash#后部分中获取参数并使用innerHTML或eval动态更新页面内容。攻击者构造一个恶意URL用户访问后即中招。特点不经过服务器纯前端漏洞WAFWeb应用防火墙难以防护。XSS的危害盗取用户会话窃取Cookie、LocalStorage中的认证信息直接冒充用户登录。钓鱼攻击在页面中插入伪造的登录框诱骗用户输入账号密码。劫持用户操作利用JavaScript模拟用户点击、提交表单。传播恶意软件通过插入恶意脚本下载并执行木马。破坏页面内容与样式篡改网页显示影响企业形象。在JeecgBoot中的常见风险点富文本内容展示使用v-htmlVue或th:utextThymeleaf等指令直接渲染未净化的HTML内容。API接口响应后端返回给前端的JSON数据中包含未转义的用户可控字段前端直接将其插入DOM。错误的消息提示将用户输入或URL参数直接用于alert()或页面提示。注意很多开发者认为用了Vue/React等现代框架就天然防XSS这是误区。框架默认的插值{{ }}或属性绑定确实会转义但当你使用v-html、dangerouslySetInnerHTML或直接操作innerHTML时这道安全门就被手动打开了。3. JeecgBoot安全防护体系构建从框架配置到编码规范理解了威胁我们就可以系统地构建防御工事。安全防护是一个多层次、纵深防御的体系不能只依赖单一手段。3.1 第一道防线输入验证与过滤白名单原则在数据进入业务逻辑之前进行清洗是最有效且成本最低的防护方式。核心原则是“白名单”优于“黑名单”。即只允许已知安全的字符通过而不是试图拦截所有已知危险的字符。1. 使用JSR-303 Bean Validation进行基础校验JeecgBoot集成Spring Boot可以很方便地使用注解进行校验。这不仅能防攻击也是保证数据质量的基础。Data public class UserQueryDTO { NotBlank(message 用户名不能为空) Size(min 2, max 20, message 用户名长度2-20位) Pattern(regexp ^[a-zA-Z0-9_]$, message 用户名只能包含字母、数字和下划线) // 白名单正则 private String username; Email(message 邮箱格式不正确) private String email; // 对于分页排序字段可以限定可选值 private String sortField; // 前端传入需要在Controller或Service中做白名单校验 }在Controller中使用Validated注解触发校验PostMapping(/query) public Result? queryUsers(Validated RequestBody UserQueryDTO dto) { // 参数校验通过后才执行逻辑 return Result.OK(userService.listUsers(dto)); }2. 自定义全局过滤器进行通用过滤对于所有请求参数可以创建一个Servlet Filter或Spring Interceptor对参数中的潜在危险字符进行过滤或转义。但要注意这种方式可能误伤正常业务比如一篇技术文章里确实需要包含script这个词因此主要用于拦截明显恶意攻击不能替代业务层的精准校验。Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; // 使用包装类对请求参数进行包装和过滤 XssHttpServletRequestWrapper wrappedRequest new XssHttpServletRequestWrapper(httpRequest); chain.doFilter(wrappedRequest, response); } } // XssHttpServletRequestWrapper 需要重写 getParameter, getParameterValues, getHeader 等方法在其中对字符串进行HTML转义或使用Jsoup等库进行清理。3. 针对SQL注入的专用过滤对于排序字段、表名等动态部分必须采用白名单映射。Service public class UserServiceImpl implements UserService { // 定义允许排序的字段白名单 private static final SetString ALLOWED_SORT_FIELDS new HashSet(Arrays.asList(createTime, updateTime, username)); public PageUser queryUsers(PageUser page, String sortField, String sortOrder) { QueryWrapperUser queryWrapper new QueryWrapper(); // 校验排序字段 if (StringUtils.isNotBlank(sortField)) { if (!ALLOWED_SORT_FIELDS.contains(sortField)) { sortField createTime; // 或抛出异常 } boolean isAsc asc.equalsIgnoreCase(sortOrder); queryWrapper.orderBy(true, isAsc, sortField); } return userMapper.selectPage(page, queryWrapper); } }3.2 第二道防线安全的数据处理与持久化使用预编译这是防御SQL注入的最核心、最有效的手段没有之一。1. 严格使用MyBatis-Plus的#{}语法在MyBatis/MyBatis-Plus的XML映射文件或注解中#{}会将参数预编译为占位符?然后由数据库驱动来安全地设置参数值从根本上杜绝SQL拼接。!-- 安全 -- select idselectUserByName resultTypeUser SELECT * FROM sys_user WHERE username #{name} /select !-- 危险仅在动态表名、字段名等无法使用#{}的场景下谨慎使用且必须结合白名单 -- select idselectByDynamicColumn resultTypemap SELECT * FROM ${tableName} ORDER BY ${sortField} !-- ${} 是直接拼接 -- /select2. 使用QueryWrapper/LambdaQueryWrapper的正确姿势MyBatis-Plus的Wrapper提供了类型安全、链式调用的查询构建方式它内部也是使用预编译。// 安全使用Lambda编译时检查 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, usernameParam); // 这里传入的参数会被预编译处理 wrapper.like(User::getRealname, % realnameParam %); // like参数需要自己拼接%但realnameParam本身是预编译的 // 危险手动拼接SQL片段慎用 QueryWrapperUser badWrapper new QueryWrapper(); badWrapper.apply(date_format(create_time,%Y-%m-%d) dateParam ); // 直接拼接字符串到SQL中 // 应改为 badWrapper.apply(date_format(create_time,%Y-%m-%d) {0}, dateParam); // 使用{0}占位符MyBatis-Plus会进行预编译处理3. 防范模糊查询中的通配符转义当用户输入可能包含%或_时如果业务上不希望它们被解释为通配符需要进行转义。public ListUser searchUsers(String keyword) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); // 转义keyword中的特殊字符使其作为普通字符搜索 String escapedKeyword keyword.replace(%, \\%).replace(_, \\_); wrapper.like(User::getRealname, % escapedKeyword %); return userMapper.selectList(wrapper); }3.3 第三道防线安全的输出编码与响应设置数据安全地存进去了还要安全地吐出来。这是防御XSS的关键。1. 前端渲染时的HTML编码Vue.js (JeecgBoot前端常用)默认的模板插值{{ data }}会对数据进行HTML转义。绝对避免在用户可控内容上使用v-html指令。如果必须渲染富文本必须使用可靠的XSS过滤库如DOMPurify在渲染前进行净化。!-- 安全 -- template div{{ userInput }}/div !-- 会被转义显示为纯文本 -- /template !-- 危险 -- template div v-htmluserInput/div !-- 直接作为HTML解析可能执行脚本 -- /template !-- 相对安全需配合净化库 -- template div v-htmlpurifiedHtml/div /template script import DOMPurify from dompurify; export default { computed: { purifiedHtml() { return DOMPurify.sanitize(this.userInput); } } } /scriptThymeleaf (后端模板)使用th:text而非th:utext。th:text会自动进行HTML转义。!-- 安全 -- p th:text${content}/p !-- 危险 -- p th:utext${content}/p2. 设置安全的HTTP响应头通过Spring Security或自定义过滤器为响应添加安全头利用浏览器的安全特性进行防护。Content-Security-Policy (CSP)最强大的XSS缓解措施之一。它可以定义页面允许加载哪些来源的资源脚本、样式、图片等即使恶意脚本被注入浏览器也不会执行。// 在Spring Security配置中 http.headers() .contentSecurityPolicy(default-src self; script-src self https://trusted.cdn.com; object-src none;);这个策略表示默认只允许加载同源资源脚本只允许同源和https://trusted.cdn.com禁止加载object等插件。这能有效阻止内联脚本和外部恶意脚本的执行。X-Content-Type-Options: nosniff阻止浏览器进行MIME类型嗅探降低基于文件上传的XSS风险。X-Frame-Options: DENY/SAMEORIGIN防止页面被嵌入到frame,iframe,embed,object中用于对抗点击劫持。X-XSS-Protection: 1; modeblock启用旧版浏览器的XSS过滤功能并在检测到攻击时阻止页面加载。3.4 第四道防线框架与组件安全加固充分利用JeecgBoot和Spring生态已有的安全能力。1. 启用并正确配置Spring SecurityJeecgBoot默认集成了Shiro/Apache Shiro但Spring Security是Spring官方更主流、生态更丰富的安全框架。如果项目复杂度高可以考虑迁移或深入配置Shiro。CSRF防护确保POST、PUT、DELETE等非幂等请求开启了CSRF防护。Spring Security默认是开启的。会话管理设置合理的会话超时时间、防止会话固定攻击。密码安全使用强哈希算法如BCrypt存储密码绝对禁止明文存储。2. 谨慎处理文件上传文件上传是另一个高风险点可能造成恶意文件上传、存储型XSS上传HTML/JS文件等。校验文件类型不要仅依赖文件扩展名应检查文件内容的魔数Magic Number或使用Files.probeContentType()。重命名文件使用UUID等随机名称存储文件避免用户通过文件名猜测路径。设置独立域名和存储路径将用户上传的文件存放在独立的域名或路径下并使用静态资源服务器如Nginx提供访问限制其执行权限如不可执行。图片处理对上传的图片进行二次压缩或转换破坏可能隐藏的脚本代码。3. 依赖库安全管理定期使用mvn dependency:check或GitHub Dependabot、OWASP Dependency-Check等工具扫描项目依赖及时更新存在已知漏洞的第三方库版本。4. 实战演练在JeecgBoot中实施全链路防护让我们结合一个JeecgBoot中常见的“用户管理-高级查询”功能将上述策略串联起来进行一次完整的防护实战。场景一个后台用户列表页面支持根据用户名模糊查询、状态、创建时间范围进行筛选并支持动态排序。4.1 第一步定义安全的DTO数据传输对象Data public class UserQueryDTO { Size(max 50, message 用户名搜索词过长) private String username; Range(min 0, max 2, message 用户状态值不合法) private Integer status; DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) private Date createTimeBegin; DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) private Date createTimeEnd; // 排序字段和顺序需要在业务层做白名单校验 private String sortField createTime; // 默认值 private String sortOrder desc; // 默认值 }4.2 第二步Controller层进行参数校验与绑定RestController RequestMapping(/sys/user) Slf4j public class SysUserController { Autowired private ISysUserService userService; GetMapping(/list) public ResultIPageSysUser queryPageList(UserQueryDTO queryDTO, RequestParam(namepageNo, defaultValue1) Integer pageNo, RequestParam(namepageSize, defaultValue10) Integer pageSize) { // Validated 注解会在参数绑定后自动校验DTO中的注解 // 这里我们手动调用校验或使用Validated在方法参数上 // 更佳实践是使用Validated注解 PageSysUser page new Page(pageNo, pageSize); IPageSysUser pageList userService.queryUserPage(page, queryDTO); return Result.OK(pageList); } }4.3 第三步Service层实现安全的业务逻辑核心Service public class SysUserServiceImpl extends ServiceImplSysUserMapper, SysUser implements ISysUserService { // 允许排序的字段白名单 private static final MapString, String ALLOWED_SORT_FIELDS new HashMap(); static { ALLOWED_SORT_FIELDS.put(username, username); ALLOWED_SORT_FIELDS.put(realname, realname); ALLOWED_SORT_FIELDS.put(createTime, create_time); // 数据库字段名 ALLOWED_SORT_FIELDS.put(updateTime, update_time); } Override public IPageSysUser queryUserPage(PageSysUser page, UserQueryDTO queryDTO) { LambdaQueryWrapperSysUser queryWrapper new LambdaQueryWrapper(); // 1. 处理模糊查询 - 转义通配符 if (StringUtils.isNotBlank(queryDTO.getUsername())) { String escapedUsername escapeSqlWildcard(queryDTO.getUsername()); queryWrapper.like(SysUser::getUsername, % escapedUsername %); } // 2. 处理等值查询 - 直接使用预编译 if (queryDTO.getStatus() ! null) { queryWrapper.eq(SysUser::getStatus, queryDTO.getStatus()); } // 3. 处理范围查询 - 参数预编译 if (queryDTO.getCreateTimeBegin() ! null) { queryWrapper.ge(SysUser::getCreateTime, queryDTO.getCreateTimeBegin()); } if (queryDTO.getCreateTimeEnd() ! null) { queryWrapper.le(SysUser::getCreateTime, queryDTO.getCreateTimeEnd()); } // 4. 处理动态排序 - 白名单校验 String sortField queryDTO.getSortField(); String dbSortField ALLOWED_SORT_FIELDS.get(sortField); if (dbSortField null) { dbSortField create_time; // 默认排序字段 } boolean isAsc asc.equalsIgnoreCase(queryDTO.getSortOrder()); // 使用 orderBy 方法传入SQL片段但字段名来自白名单是安全的 queryWrapper.orderBy(true, isAsc, dbSortField); // 或者使用 last() 方法但要确保参数安全 // queryWrapper.last(order by dbSortField (isAsc ? asc : desc)); // 5. 执行查询MyBatis-Plus内部使用预编译安全 return this.page(page, queryWrapper); } /** * 转义SQL LIKE子句中的通配符 */ private String escapeSqlWildcard(String input) { if (input null) { return null; } // 转义 % 和 _ 以及 \ (MySQL中\也是转义符) return input.replace(\\, \\\\) .replace(%, \\%) .replace(_, \\_); } }4.4 第四步前端安全渲染假设前端使用Vue。template div !-- 搜索表单 -- a-form-model :modelqueryParam submithandleSearch a-form-model-item label用户名 a-input v-modelqueryParam.username placeholder支持模糊查询 / /a-form-model-item !-- ... 其他表单项 ... -- /a-form-model !-- 用户列表 -- a-table :columnscolumns :data-sourcedataList template slotusername slot-scopetext !-- 使用 {{ }} 插值自动HTML转义安全 -- {{ text }} /template template slotaction slot-scoperecord !-- 任何操作链接都应避免将用户可控数据直接拼入href或事件 -- a clickshowDetail(record.id)查看/a !-- 错误示例a :href/user/ record.username查看/a 如果username可控可能构造XSS -- /template /a-table /div /template script export default { data() { return { queryParam: {}, dataList: [], columns: [ { title: 用户名, dataIndex: username, scopedSlots: { customRender: username } }, { title: 真实姓名, dataIndex: realname }, // 直接显示框架会转义 // ... 其他列 ] }; }, methods: { handleSearch() { // 发起请求参数会被自动编码 this.$http.post(/sys/user/list, this.queryParam).then(res { this.dataList res.result.records; }); }, showDetail(id) { // 使用路由参数而非拼接字符串 this.$router.push({ path: /user/detail, query: { id: id } }); } } }; /script4.5 第五步补充全局安全配置在application.yml或通过配置类添加安全响应头# 如果使用Spring Security配置更强大。这里展示通过Servlet Filter的方式简化 # 实际项目中建议使用Spring Security的配置 server: servlet: session: cookie: http-only: true # 防止JS读取Cookie secure: true # 仅在HTTPS下传输Cookie生产环境开启创建一个简单的CSP过滤器Component public class SecurityHeaderFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) res; // 设置CSP仅允许从同源加载资源禁止内联脚本和样式 response.setHeader(Content-Security-Policy, default-src self; script-src self; style-src self unsafe-inline; img-src self data:;); response.setHeader(X-Content-Type-Options, nosniff); response.setHeader(X-Frame-Options, SAMEORIGIN); response.setHeader(X-XSS-Protection, 1; modeblock); chain.doFilter(req, res); } }5. 常见问题排查与高级防护技巧即使按照最佳实践编码在复杂的业务场景和第三方组件集成中安全问题仍可能潜伏。以下是一些实战中遇到的典型问题及排查思路。5.1 SQL注入漏洞排查清单当你怀疑某个接口可能存在SQL注入时可以按以下步骤排查定位动态SQL全局搜索代码中的${}MyBatis、.apply()MyBatis-Plus Wrapper、字符串拼接操作SQL的地方。追溯参数来源检查这些动态部分接收的参数是否来自用户输入HttpServletRequest.getParameter、RequestParam、PathVariable、前端JSON字段等。验证过滤逻辑如果参数来自用户检查是否有严格的白名单校验或强类型转换。对于排序字段、表名白名单是否覆盖了所有可能值测试验证使用SQLMap等工具仅在授权测试环境下使用或手动构造异常参数进行测试。例如对于数字型参数尝试输入1 OR 11对于字符串型尝试输入 OR 11。观察应用日志中生成的SQL语句或者是否抛出数据库异常。一个容易被忽略的盲点MyBatis的if标签内的test表达式select idselectUsers parameterTypemap resultTypeUser SELECT * FROM user where if testorderBy ! null and orderBy ! ORDER BY ${orderBy} !-- 这里用${}高危 -- /if /where /selectorderBy来自用户输入这里就是注入点。必须改为白名单校验。5.2 XSS漏洞排查清单查找数据输出点在前端代码中搜索.innerHTML、.outerHTML、document.write()、eval()、setTimeout()/setInterval()中拼接字符串、Vue的v-html、React的dangerouslySetInnerHTML。检查数据流这些输出点的数据来源是否来自API接口API接口返回的数据是否包含用户可控字段如昵称、评论内容、文章正文验证编码/过滤用户可控数据在输出前是否经过了HTML编码或净化对于富文本净化规则是否足够严格是否允许script、onerror等属性测试Payload在可输入点尝试提交以下内容查看输出结果scriptalert(1)/script基础检测img srcx onerroralert(1)利用标签属性scriptalert(1)/script尝试闭合原有标签javascript:alert(1)在href等属性中测试5.3 高级防护与监控使用专业的WAFWeb应用防火墙在应用层之前部署WAF如ModSecurity开源、云厂商提供的WAF服务。它可以基于规则库拦截常见的SQL注入、XSS等攻击请求为应用提供一道额外的缓冲。但记住WAF不能替代安全的代码它是“安全带”不是“不撞车”的保证。实施RASP运行时应用自我保护在应用内部部署探针监控运行时行为。当检测到有SQL注入、命令注入等危险操作时可以实时阻断并告警。一些Java Agent工具可以实现此功能。完善的日志审计记录所有用户的关键操作尤其是登录、数据导出、敏感信息修改、接收到的异常参数以及后端生成的最终SQL语句需脱敏敏感数据。当发生安全事件时日志是溯源分析的唯一依据。JeecgBoot的日志框架可以很方便地集成此功能。定期安全扫描与渗透测试在项目上线前和定期维护中使用自动化漏洞扫描工具如OWASP ZAP、Burp Suite对系统进行扫描。条件允许的话聘请专业的安全团队进行渗透测试他们能发现自动化工具找不到的逻辑漏洞。保持依赖更新与安全通告关注订阅JeecgBoot社区、Spring、MyBatis等核心依赖的安全邮件列表或公告及时修复已知漏洞。安全防护是一个持续的过程而非一劳永逸的任务。在JeecgBoot项目开发中将安全思维融入需求评审、架构设计、编码实现、测试验证和运维监控的每一个环节才能构建出真正健壮可靠的应用系统。从今天起每次写下一行代码都多问一句“用户输入从这里进来它会去哪里它安全吗”