从开发视角看安全:我的Spring Boot项目是如何一步步堵上SQL注入、XSS、越权这些坑的

发布时间:2026/6/10 16:37:17

从开发视角看安全:我的Spring Boot项目是如何一步步堵上SQL注入、XSS、越权这些坑的 从开发视角看安全我的Spring Boot项目是如何一步步堵上SQL注入、XSS、越权这些坑的在开发一个用户管理系统的过程中安全问题往往不是一开始就考虑周全的。作为后端开发者我们更关注功能的快速实现和性能优化直到某次安全扫描报告亮起红灯或是线上出现安全事件才会真正重视起来。本文将分享我在开发Spring Boot项目时如何从零开始逐步完善安全防护解决SQL注入、XSS、越权等常见漏洞的真实经历。1. SQL注入从拼接SQL到预编译的转变最初实现用户查询功能时为了快速上线我直接使用了字符串拼接的方式构建SQL语句// 危险示例字符串拼接SQL GetMapping(/users) public ListUser getUsers(RequestParam String name) { String sql SELECT * FROM users WHERE name name ; return jdbcTemplate.query(sql, new UserRowMapper()); }直到安全团队用简单的测试用例nameadmin OR 11就获取了全部用户数据我才意识到问题的严重性。修复方案很简单但极其有效// 安全方案使用预编译语句 GetMapping(/users) public ListUser getUsers(RequestParam String name) { String sql SELECT * FROM users WHERE name ?; return jdbcTemplate.query(sql, new Object[]{name}, new UserRowMapper()); }在MyBatis中同样需要注意${}和#{}的区别!-- 危险用法 -- select idfindByName parameterTypeString resultTypeUser SELECT * FROM users WHERE name ${name} /select !-- 安全用法 -- select idfindByName parameterTypeString resultTypeUser SELECT * FROM users WHERE name #{name} /select关键防御措施始终使用预编译语句PreparedStatementMyBatis中优先使用#{}语法对必须使用动态表名/列名的场景严格白名单校验最小化数据库账号权限2. XSS防护从基础转义到内容安全策略用户评论功能上线后不久运营同事报告有用户昵称显示异常。检查发现有人提交了这样的内容scriptalert(XSS攻击)/script第一层防御是在服务端对输出进行HTML转义// Spring Boot默认的Thymeleaf模板已自动转义 // 手动转义示例 import org.springframework.web.util.HtmlUtils; public String safeOutput(String input) { return HtmlUtils.htmlEscape(input); }但转义并不能解决所有场景。比如用户需要在评论中使用富文本时我们采用了更精细的防护引入JSoup库进行HTML过滤// 只允许安全的HTML标签和属性 String safeHtml Jsoup.clean(unsafeHtml, Whitelist.basicWithImages() .addAttributes(a, href, title) .addProtocols(a, href, http, https));设置HTTP安全头增强防护// 在Spring Security配置中添加 http.headers() .xssProtection() .contentSecurityPolicy(default-src self; script-src self unsafe-inline);多维度防护方案响应头设置X-XSS-Protection, Content-Security-Policy前端框架如React/Vue的自动转义富文本场景使用白名单过滤Cookie设置HttpOnly属性3. 越权漏洞从简单判断到系统化权限控制最初的用户信息接口只验证了用户是否登录没有校验操作的资源是否属于当前用户// 危险示例未校验用户权限 GetMapping(/users/{userId}) public User getUser(PathVariable Long userId) { return userRepository.findById(userId).orElse(null); }这导致了水平越权漏洞——任何登录用户只需修改userId参数就能查看他人信息。修复方案// 基础权限校验 GetMapping(/users/{userId}) public User getUser(PathVariable Long userId, AuthenticationPrincipal User currentUser) { if (!currentUser.getId().equals(userId)) { throw new AccessDeniedException(无权访问该用户信息); } return userRepository.findById(userId).orElse(null); }随着业务复杂化我们引入了Spring Security的权限系统// 基于注解的细粒度控制 PreAuthorize(hasRole(ADMIN) or #userId principal.id) GetMapping(/users/{userId}) public User getUser(PathVariable Long userId) { // ... } // 方法级权限控制 PreAuthorize(permissionService.canAccessUser(principal, #userId)) GetMapping(/users/{userId}/details) public UserDetail getUserDetails(PathVariable Long userId) { // ... }权限控制最佳实践遵循最小权限原则服务端校验永远比前端校验可靠对敏感操作记录详细日志定期审计权限配置4. CSRF防护从手动Token到框架集成在发现通过伪造请求可以执行用户非预期的操作后我们首先手动实现了CSRF Token!-- 前端表单中添加Token -- form action/transfer methodpost input typehidden name${_csrf.parameterName} value${_csrf.token}/ !-- 其他表单字段 -- /form// 服务端校验 PostMapping(/transfer) public void transferMoney(Valid TransferRequest request, RequestParam(_csrf) String csrfToken) { // 验证CSRF Token if (!csrfToken.equals(session.getAttribute(CSRF_TOKEN))) { throw new SecurityException(Invalid CSRF token); } // 处理业务逻辑 }后来发现Spring Security已经提供了完善的CSRF防护只需简单配置Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 其他配置... }CSRF防护要点重要操作使用POST/PUT/DELETE方法同源策略检查敏感操作二次认证避免GET请求修改状态5. 文件上传安全从简单后缀检查到全流程防护用户头像上传功能最初只检查了文件后缀// 不安全的检查方式 String ext FilenameUtils.getExtension(filename); if (!Arrays.asList(jpg, png).contains(ext)) { throw new IllegalArgumentException(不支持的文件类型); }这种防护很容易绕过我们升级为更全面的检查文件内容类型验证// 使用Tika检测真实文件类型 InputStream is file.getInputStream(); ContentType contentType new Tika().detect(is); if (!image/jpeg.equals(contentType.toString())) { throw new IllegalArgumentException(非图片文件); }文件存储安全处理// 生成随机文件名并限制访问权限 String safeFilename UUID.randomUUID() .jpg; Path dest Paths.get(/var/upload, safeFilename); Files.copy(file.getInputStream(), dest, StandardCopyOption.REPLACE_EXISTING); // 设置文件权限 Files.setPosixFilePermissions(dest, PosixFilePermissions.fromString(rw-r-----));服务端图片二次处理// 使用Thumbnailator重写图片文件 Thumbnails.of(inputStream) .size(200, 200) .outputFormat(jpg) .toOutputStream(outputStream);文件上传安全措施文件类型白名单校验文件内容真实检测随机化存储文件名限制文件访问权限图片文件重压缩处理独立文件服务器部署6. 日志与监控安全问题的最后防线即使做了各种防护仍然需要完善的日志系统作为最后保障。我们在关键位置添加了安全日志Aspect Component public class SecurityLogAspect { AfterReturning( pointcut execution(* com.example..*(..)) annotation(securityLog), returning result) public void logAfter(JoinPoint joinPoint, SecurityLog securityLog, Object result) { String method joinPoint.getSignature().toShortString(); String params Arrays.toString(joinPoint.getArgs()); // 记录到专门的审计日志 auditLogger.info(操作[{}], 参数[{}], 结果[{}], method, params, result); } AfterThrowing( pointcut execution(* com.example..*(..)), throwing ex) public void logException(JoinPoint joinPoint, Throwable ex) { if (ex instanceof AccessDeniedException) { securityAlert.warn(权限拒绝访问: {}, joinPoint.getSignature(), ex.getMessage()); } } }同时配置了ELK日志系统设置关键安全事件的告警规则# 示例告警规则 alert: name: 多次登录失败 condition: count by source_ip ( status FAILURE and event LOGIN_ATTEMPT ) 5 action: notify_security_team(source_ip)安全是一个持续的过程需要开发者在每个功能迭代中都保持警惕。从我的经验来看最有效的安全策略不是复杂的防护体系而是开发团队对安全问题的持续关注和及时响应。

相关新闻