Spring Boot应用XSS与SQL注入防护实战指南

Spring Boot应用XSS与SQL注入防护实战指南

1. 项目概述:为什么Spring Boot应用必须重视XSS与SQL注入防护?

在Web应用开发领域,尤其是使用Spring Boot这类高效框架时,我们常常把精力集中在业务逻辑实现、性能优化和微服务架构上。然而,一个功能再强大的应用,如果其安全防线千疮百孔,那么所有的努力都可能瞬间化为乌有。XSS(跨站脚本攻击)和SQL注入,正是悬在Web应用头顶的两把“达摩克利斯之剑”。它们并非什么高深莫测的黑客技术,反而是利用开发者疏忽、最常见也最危险的攻击手段。

我见过太多项目,在开发阶段对安全防护草草了事,上线后一旦遭遇攻击,轻则数据泄露、页面篡改,重则服务器被控、核心业务瘫痪,造成的损失和声誉影响难以估量。Spring Boot通过自动配置和约定大于配置的理念,极大地简化了开发,但它并不会自动为你筑起坚固的安全城墙。防护XSS和SQL注入,是开发者必须主动承担的责任。

简单来说,XSS攻击是让恶意脚本在用户的浏览器中执行,从而盗取用户会话、篡改页面内容或进行钓鱼欺诈。而SQL注入则是通过构造特殊的输入,欺骗后端数据库执行非预期的SQL命令,达到窃取、篡改甚至删除数据的目的。这两种攻击的根源,都来自于对用户输入数据的不信任和未经验证的处理。

本篇文章,我将结合十多年的实战经验,为你彻底拆解在Spring Boot项目中,如何从编码习惯、框架特性、组件选型到部署配置,构建一套立体、可落地的XSS与SQL注入防护体系。这不是一篇照本宣科的理论文章,而是能让你“抄作业”的实战指南,涵盖从原理到避坑的完整闭环。

2. 安全威胁深度解析:XSS与SQL注入的攻击原理与危害

在动手搭建防护体系之前,我们必须先成为“攻击者”,深刻理解对手是如何工作的。只有知己知彼,才能构建有效的防御。

2.1 XSS攻击:信任的滥用与脚本的“越狱”

XSS的本质是“HTML注入”。攻击者发现Web应用在将用户输入的内容(如评论、搜索关键词、URL参数)输出到HTML页面时,没有进行正确的转义或过滤,从而使得输入内容中的HTML标签或JavaScript脚本被浏览器解析并执行。

2.1.1 反射型XSS:一次性的“钓鱼钩”

这是最常见的一种。攻击者构造一个包含恶意脚本的URL,并通过邮件、社交网站等渠道诱骗用户点击。当用户点击这个链接,服务器接收到恶意参数后,未加处理就直接将其拼接进响应页面并返回给用户的浏览器,脚本随即执行。

<!-- 假设一个搜索功能,URL为:/search?keyword=用户输入 --> <!-- 正常搜索:/search?keyword=SpringBoot --> <!-- 恶意攻击:/search?keyword=<script>alert('XSS')</script> -->

如果后端代码简单地return “你搜索的关键词是:” + keyword;, 那么<script>标签就会被浏览器执行。在实际攻击中,这里的脚本可能是盗取用户Cookie并发送到攻击者服务器的代码。

2.1.2 存储型XSS:持久化的“毒药”

这种攻击更为致命。恶意脚本被提交并存储到服务器的数据库或文件系统中(如论坛帖子、用户昵称、商品评论)。此后,任何其他用户在浏览包含该数据的页面时,都会触发恶意脚本的执行。它的危害范围广、持续时间长。例如,攻击者在博客评论中插入一段窃取登录态的脚本,之后所有查看该评论的用户都可能中招。

2.1.3 DOM型XSS:客户端的“内鬼”

这种攻击不经过服务器。恶意数据在客户端被JavaScript动态操作DOM时不当使用,导致脚本执行。例如:

// 从URL的hash中获取参数并直接写入DOM var input = window.location.hash.substring(1); document.getElementById(“message”).innerHTML = input;

如果用户访问的URL是example.com#<img src=1 onerror=alert(‘XSS’)>, 那么onerror事件中的脚本就会执行。

实操心得:很多开发者只防反射型和存储型,忽略了DOM型XSS。尤其是在大量使用前端框架(如Vue、React)进行动态渲染时,如果安全意识不足,很容易引入此类漏洞。防护的关键在于,任何来自不可信源(URL参数、Cookie、第三方API返回)的数据,在用于操作DOM(如innerHTML, document.write, eval)前,都必须进行净化或使用安全的API(如textContent)。

2.2 SQL注入:与数据库的“非法对话”

SQL注入的原理更为直接:攻击者在应用程序的输入字段(如登录框、搜索框)中,插入精心构造的SQL代码片段。当应用程序将这些输入未经处理直接拼接到SQL查询语句中时,攻击者的代码就成为原始查询的一部分,被数据库执行。

2.2.1 经典注入:绕过身份验证假设登录查询的Java代码是:

String sql = “SELECT * FROM users WHERE username = ‘” + username + “‘ AND password = ‘” + password + “‘”;

攻击者在用户名输入admin’ —(注意–是SQL注释符),密码任意。拼接后的SQL变为:

SELECT * FROM users WHERE username = ‘admin’ — ‘ AND password = ‘xxx’

之后的内容被注释掉,攻击者就能以管理员身份登录,无需密码。

2.2.2 联合查询注入:窃取数据攻击者利用UNION SELECT语句,将恶意查询结果附加到原始查询结果之后,从而读取其他表的数据。例如,输入1′ UNION SELECT username, password FROM users —

2.2.3 盲注:没有回显的“猜谜”当页面不会直接显示数据库错误信息或查询结果时,攻击者通过构造让SQL语句执行结果在页面响应(如真/假、时间延迟)上产生差异的Payload,来一点点“盲猜”出数据库的结构和内容。例如,输入1′ AND SLEEP(5) —, 如果页面响应延迟了5秒,说明注入成功。

2.2.4 危害升级:从数据泄露到服务器沦陷成功的SQL注入远不止窃取数据。攻击者可以利用数据库的特性执行更危险的操作:

  • 读写服务器文件:利用LOAD_FILE()INTO OUTFILE函数。
  • 执行系统命令:在某些数据库配置下(如SQL Server的xp_cmdshell),通过注入执行操作系统命令,直接控制服务器。
  • 绕过WAF:通过编码、注释符混淆、等价函数替换等方式,绕过Web应用防火墙的检测规则。

避坑指南:永远不要抱有“我的SQL语句很简单,不会被注入”的侥幸心理。任何将用户输入直接拼接成SQL字符串的地方,都是潜在的风险点。即使是ORDER BY后面的字段名、LIMIT后面的分页参数,如果来自用户输入且未经验证,也可能成为注入点。

3. Spring Boot立体防护体系构建:从编码到部署

理解了攻击原理,我们就可以针对性地在Spring Boot应用的各个层面布防。防护不是单一技术,而是一个从数据流入到流出的完整链条。

3.1 第一道防线:输入验证与数据绑定

在数据刚进入应用时就进行严格的校验,可以将大量畸形、恶意的请求拒之门外。

3.1.1 使用JSR 380 (Bean Validation 2.0) 进行声明式验证Spring Boot天然整合了Hibernate Validator。不要只在保存数据时验证,在Controller层接收参数时就应该开始。

import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; @Data public class UserDTO { @NotBlank(message = “用户名不能为空”) @Size(min = 3, max = 20, message = “用户名长度3-20位”) @Pattern(regexp = “^[a-zA-Z0-9_]+$”, message = “用户名只能包含字母、数字和下划线”) // 白名单规则,拒绝特殊字符 private String username; // 对于搜索关键词,可以限制长度和字符集 @Size(max = 100, message = “搜索词过长”) @Pattern(regexp = “^[\\u4e00-\\u9fa5a-zA-Z0-9\\s\\-]+$”, message = “包含非法字符”) private String keyword; }

在Controller中,使用@Valid注解触发验证:

@PostMapping(“/register”) public ResponseEntity<?> register(@RequestBody @Valid UserDTO userDTO, BindingResult result) { if (result.hasErrors()) { // 返回详细的验证错误信息,帮助前端提示,但日志中要记录异常请求 return ResponseEntity.badRequest().body(result.getAllErrors()); } // 业务逻辑... }

3.1.2 自定义验证器应对复杂场景对于更复杂的验证逻辑,如业务规则校验、黑名单词过滤,可以创建自定义验证器。

@Documented @Constraint(validatedBy = XssSafeValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface XssSafe { String message() default “内容包含潜在的不安全脚本”; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } public class XssSafeValidator implements ConstraintValidator<XssSafe, String> { // 这里可以引入一个简单的正则或关键词库进行初步过滤 private static final Pattern[] XSS_PATTERNS = { Pattern.compile(“<script>”, Pattern.CASE_INSENSITIVE), Pattern.compile(“javascript:”, Pattern.CASE_INSENSITIVE), Pattern.compile(“on\\w+=”, Pattern.CASE_INSENSITIVE) // onclick, onerror等 }; @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) return true; for (Pattern pattern : XSS_PATTERNS) { if (pattern.matcher(value).find()) { // 记录安全日志 log.warn(“检测到潜在的XSS攻击输入: {}”, value); return false; } } return true; } }

注意事项:输入验证应采用“白名单”原则(只允许已知好的字符),而非“黑名单”(试图阻止已知坏的字符)。黑名单永远无法穷尽所有攻击变种。上述自定义验证器仅作为补充和日志记录手段,绝不能作为唯一的XSS防护措施。

3.2 核心防御:输出编码与参数化查询

这是防护XSS和SQL注入最核心、最有效的手段。

3.2.1 防御XSS:在正确的上下文中进行输出编码XSS防护的黄金法则是:任何不可信的数据在输出到不同上下文时,必须使用对应的编码函数。

  • HTML上下文:将<,>,&,,等字符转换为HTML实体(如<->&lt;)。
  • HTML属性上下文:除了HTML实体转义,还要注意属性值用引号包裹。
  • JavaScript上下文:需要将数据放入JS字符串时,需进行JS转义(如\”,\\,\n)。
  • URL上下文:进行URL编码(百分号编码)。

在Spring Boot中,我们有多种实现方式:

方案一:依赖模板引擎的自动转义(推荐)如果你使用Thymeleaf(Spring Boot默认推荐),它默认对所有表达式输出进行HTML转义。这是最简单有效的防护。

<!-- 在Thymeleaf模板中,以下方式是安全的 --> <p th:text=”${userInput}”></p> <!-- 自动转义 --> <div th:utext=”${trustedHtml}”></div> <!-- utext 表示不转义,慎用! -->

确保你的Thymeleaf配置没有关闭转义(默认就是开启的)。对于Freemarker和Velocity,也需要确认其自动转义功能已启用。

方案二:在服务端手动编码后输出如果前后端分离,后端提供JSON API,那么需要在将数据放入响应体之前,或者在JSON序列化过程中进行编码。可以使用org.springframework.web.util.HtmlUtils

import org.springframework.web.util.HtmlUtils; public class ApiResponse { private String content; // Getter中编码 public String getContent() { return HtmlUtils.htmlEscape(this.content); // 进行HTML转义 } // 或者,在设置内容时就编码 public void setSafeContent(String rawContent) { this.content = HtmlUtils.htmlEscape(rawContent); } }

对于更复杂的场景,可以考虑使用OWASP Java Encoder项目提供的Encoder类,它提供了针对不同上下文的编码方法。

import org.owasp.encoder.Encode; // ... String safeForHtml = Encode.forHtmlContent(userInput); String safeForJs = Encode.forJavaScript(userInput); String safeForAttr = Encode.forHtmlAttribute(userInput);

方案三:前端框架的防护现代前端框架如React、Vue、Angular,在默认情况下都会对渲染到模板中的数据自动进行转义,这提供了另一层防护。但切记,使用v-html(Vue)或dangerouslySetInnerHTML(React)等特性时,等同于告诉框架“我信任这段HTML”,此时必须确保内容来源绝对安全或已自行净化。

3.2.2 防御SQL注入:永远使用参数化查询(PreparedStatement)这是根治SQL注入的“银弹”。它的原理是将SQL语句的结构(命令、表名、列名)与数据(用户输入的值)分开发送数据库。数据库会先编译SQL结构,然后将输入的值仅仅当作“数据”来处理,而不是可执行的代码。

在Spring Boot中,我们有多种优雅的方式:

方案一:Spring Data JPA / Hibernate(最省心)使用CrudRepository或JpaRepository,其方法查询和@Query注解默认使用参数绑定,天然防注入。

public interface UserRepository extends JpaRepository<User, Long> { // 方法名查询 - 安全 User findByUsername(String username); // @Query 注解 - 使用命名参数或位置参数,安全 @Query(“SELECT u FROM User u WHERE u.username = :uname AND u.email = :email”) User findUser(@Param(“uname”) String username, @Param(“email”) String email); // 原生SQL查询 - 也必须使用参数绑定! @Query(value = “SELECT * FROM users WHERE username = ?1 AND status = ?2”, nativeQuery = true) User findNativeUser(String username, int status); }

方案二:JdbcTemplate(灵活且安全)Spring的JdbcTemplate强制要求使用参数占位符(?)和参数列表,有效防止注入。

@Autowired private JdbcTemplate jdbcTemplate; public User findUser(String username, String password) { // 正确做法:使用参数占位符 String sql = “SELECT * FROM users WHERE username = ? AND password = ?”; // jdbcTemplate会自动处理参数转义 return jdbcTemplate.queryForObject(sql, new Object[]{username, password}, new UserRowMapper()); } // 绝对禁止的写法(拼接字符串): // String sql = “SELECT * FROM users WHERE username = ‘” + username + “‘ AND password = ‘” + password + “‘”; // 高危!

方案三:MyBatis(注意${}#{}的区别)MyBatis中,#{}是参数占位符,会进行预编译,是安全的。而${}是字符串替换,直接将值拼接到SQL语句中,存在SQL注入风险,应仅用于动态指定列名、表名等不可信数据来源的场景,且使用时必须非常谨慎,最好结合白名单验证。

<!-- 安全:使用 #{} --> <select id=”selectUser” resultType=”User”> SELECT * FROM user WHERE username = #{username} </select> <!-- 危险:使用 ${} 拼接用户输入 --> <select id=”selectOrder” resultType=”Order”> SELECT * FROM orders ORDER BY ${orderBy} <!-- 如果orderBy来自用户输入,则危险! --> </select> <!-- 相对安全:使用 ${} 但参数是内部枚举或经过白名单校验 --> <select id=”selectWithDynamicTable” resultType=”Map”> SELECT * FROM ${tableName} <where> <if test=”type != null”> AND type = #{type} </if> </where> </select> // Java代码中,调用该方法前,必须对tableName进行白名单校验 public List<Map> selectFromTable(String tableName) { List<String> allowedTables = Arrays.asList(“user”, “order”, “product”); if (!allowedTables.contains(tableName.toLowerCase())) { throw new IllegalArgumentException(“Invalid table name”); } return sqlSession.selectList(“selectWithDynamicTable”, tableName); }

核心经验:在你的项目中全局搜索${(MyBatis)和字符串拼接(+)的SQL语句,这是排查SQL注入漏洞最直接有效的方法。将${}的使用限制在最小范围,并确保其参数是内部可控或经过严格白名单校验的。

3.3 增强防护:安全HTTP头、WAF与依赖管理

除了应用层代码,基础设施和配置也能提供强大的纵深防御。

3.3.1 利用安全HTTP头Spring Security可以方便地配置安全相关的HTTP响应头,它们像给浏览器下达的“安全指令”。

  • Content-Security-Policy (CSP)这是防御XSS的终极利器。它告诉浏览器只允许加载和执行来自指定来源的脚本、样式、图片等资源。即使攻击者成功注入了脚本,如果来源不在白名单内,浏览器也不会执行。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // … 其他配置 … .headers() .contentSecurityPolicy(“script-src ‘self’ https://trusted.cdn.com; object-src ‘none’;”); // 只允许同源和指定CDN的脚本,禁止插件 } }
  • X-Content-Type-Options: nosniff:阻止浏览器进行MIME类型嗅探,降低某些基于文件上传的XSS风险。
  • X-Frame-Options: DENY:防止页面被嵌入到iframe中,用于对抗点击劫持。
  • Strict-Transport-Security (HSTS):强制浏览器使用HTTPS连接。

3.3.2 考虑Web应用防火墙(WAF)对于大型或对安全要求极高的应用,可以在Spring Boot应用前部署一层WAF(如ModSecurity,或云服务商提供的WAF)。WAF基于规则库,可以识别和拦截常见的攻击模式(如SQL注入、XSS的特征字符串),为应用提供一道额外的屏障。但要注意,WAF不能替代安全的编码实践,它只是缓解措施,且可能存在误拦和绕过风险。

3.3.3 管理项目依赖,及时修复漏洞使用spring-boot-starter-parent管理版本,并定期运行mvn dependency:treegradle dependencies检查依赖,使用OWASP Dependency-Check或GitHub Dependabot等工具扫描已知漏洞(CVE)。一个脆弱的第三方库(如旧版本的MyBatis、Fastjson、Jackson)可能成为整个系统的突破口。

4. 实战演练:构建一个具备基础防护的Spring Boot API

让我们通过一个简单的用户评论API,将上述防护措施整合起来。

4.1 项目结构与依赖创建一个标准的Spring Boot项目,主要依赖:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- OWASP Encoder (可选,用于更灵活的编码) --> <dependency> <groupId>org.owasp.encoder</groupId> <artifactId>encoder</artifactId> <version>1.2.3</version> </dependency> </dependencies>

4.2 定义实体与DTO(包含输入验证)

// Comment.java (Entity) @Entity @Data public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String author; // 存储时已是安全内容 private String content; // 存储时已是安全内容 private LocalDateTime createTime; } // CommentDTO.java @Data public class CommentDTO { @NotBlank(message = “作者不能为空”) @Size(max = 50, message = “作者名过长”) @Pattern(regexp = “^[\\u4e00-\\u9fa5a-zA-Z0-9\\-\\s]+$”, message = “作者名包含非法字符”) private String author; @NotBlank(message = “评论内容不能为空”) @Size(max = 1000, message = “评论内容过长”) // 可以加上自定义的XSS初步过滤注解 @XssSafe private String content; }

4.3 实现Service层(处理业务逻辑与编码)

@Service @Slf4j public class CommentService { @Autowired private CommentRepository commentRepository; @Autowired private HtmlUtils htmlUtils; // 假设我们注入了一个工具类 public Comment createComment(CommentDTO commentDTO) { // 1. DTO的验证已在Controller层通过@Valid完成 // 2. 对即将存储到数据库的内容进行HTML转义(净化) String safeAuthor = HtmlUtils.htmlEscape(commentDTO.getAuthor().trim()); String safeContent = HtmlUtils.htmlEscape(commentDTO.getContent().trim()); // 3. 也可以使用OWASP Encoder进行更精确的编码(如果上下文复杂) // String safeContentForHtml = Encode.forHtmlContent(commentDTO.getContent()); // 记录原始输入用于审计(生产环境可存日志系统) log.debug(“Received comment - Raw: {}, Sanitized: {}”, commentDTO.getContent(), safeContent); Comment comment = new Comment(); comment.setAuthor(safeAuthor); comment.setContent(safeContent); comment.setCreateTime(LocalDateTime.now()); // 4. 使用Spring Data JPA保存,天然防SQL注入 return commentRepository.save(comment); } public List<Comment> getAllComments() { // 直接返回,因为存储时已转义。如果前端是JSON API,这里返回的对象会被序列化为JSON。 // JSON序列化器默认会对字符串进行适当的转义(如将”转义为\”),但这不同于HTML转义。 // 如果前端直接使用此JSON数据插入HTML,仍需前端进行编码。 // 更佳实践:API返回数据,由前端根据渲染上下文自行编码。 return commentRepository.findAll(); } }

4.4 实现Controller层

@RestController @RequestMapping(“/api/comments”) @Validated public class CommentController { @Autowired private CommentService commentService; @PostMapping public ResponseEntity<Comment> addComment(@RequestBody @Valid CommentDTO commentDTO, BindingResult result) { // @Valid 会自动触发验证,错误信息在result中 if (result.hasErrors()) { // 生产环境应返回更友好的错误信息,而非全部细节,防止信息泄露 throw new ValidationException(“输入参数验证失败”); } Comment savedComment = commentService.createComment(commentDTO); return ResponseEntity.ok(savedComment); } @GetMapping public ResponseEntity<List<Comment>> getComments() { return ResponseEntity.ok(commentService.getAllComments()); } }

4.5 配置全局异常处理与安全响应头

@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); // 返回400和错误信息,注意不要暴露堆栈等敏感信息 return ResponseEntity.badRequest().body(errors); } } @Configuration public class SecurityHeaderConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 可以添加拦截器进行更全局的请求/响应处理 } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.defaultContentType(MediaType.APPLICATION_JSON); } } // 通过application.properties配置安全头更简单 // server.servlet.session.cookie.http-only=true # 保护Cookie // server.servlet.session.cookie.secure=true # 仅HTTPS传输Cookie (生产环境)

5. 进阶防护与常见问题排查

即使遵循了最佳实践,在复杂的业务场景和持续迭代中,安全问题仍可能悄然出现。

5.1 富文本内容(HTML)的处理难题

用户评论、文章发布等场景需要保留一些安全的HTML格式(如加粗、链接、图片)。这时,简单的转义会破坏格式。解决方案是使用专业的HTML净化库,只允许安全的标签和属性通过。

推荐使用OWASP Java HTML Sanitizer:

import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; public class HtmlSanitizerUtil { private static final PolicyFactory POLICY = new HtmlPolicyBuilder() .allowElements(“p”, “br”, “b”, “i”, “u”, “strong”, “em”, “a”, “img”) .allowUrlProtocols(“https”, “http”) // 只允许网络URL .allowAttributes(“href”).onElements(“a”).requireRelNoFollow() // 链接增加nofollow .allowAttributes(“src”).onElements(“img”) .allowAttributes(“alt”).onElements(“img”) .allowStandardUrlProtocols() .toFactory(); public static String sanitize(String dirtyHtml) { if (dirtyHtml == null) return null; return POLICY.sanitize(dirtyHtml); // 返回安全的HTML片段 } }

在Service层,对需要富文本的字段,用sanitize()方法替换简单的htmlEscape()

踩坑记录:曾经遇到一个案例,项目使用了过时的富文本编辑器且未做净化,攻击者通过构造<img src=1 onerror=stealCookie()>这样的标签属性执行了XSS。引入Sanitizer后,onerror属性会被直接剥离。务必根据业务需求严格定义白名单标签和属性。

5.2 MyBatis中${}的动态排序与分页安全

这是SQL注入的高危区。解决方案是白名单校验

@Service public class ProductService { private static final Map<String, String> COLUMN_WHITELIST = new HashMap<>(); static { COLUMN_WHITELIST.put(“price”, “price”); COLUMN_WHITELIST.put(“createTime”, “create_time”); COLUMN_WHITELIST.put(“name”, “name”); } public List<Product> getProducts(String sortBy, String order) { // 1. 校验排序字段 String columnName = COLUMN_WHITELIST.get(sortBy); if (columnName == null) { columnName = “create_time”; // 默认值 } // 2. 校验排序方向 if (!“asc”.equalsIgnoreCase(order) && !“desc”.equalsIgnoreCase(order)) { order = “desc”; } // 3. 使用MyBatis的${},但此时参数是经过白名单校验的安全值 return productMapper.selectWithOrder(columnName, order); } } // MyBatis Mapper // <select id=”selectWithOrder” resultType=”Product”> // SELECT * FROM product ORDER BY ${sortColumn} ${sortOrder} // </select>

5.3 常见的防护误区与排查清单

误区1:只在保存时转义,读取时直接使用。正解:存储“干净”的数据(已转义或净化)。这样无论数据被如何使用、被哪个系统访问,都是安全的。这被称为“存储时净化”。

误区2:依赖前端进行XSS防护。正解:安全防护必须前后端同时进行(纵深防御),但后端必须作为最后且最可靠的防线。攻击者可以绕过浏览器直接调用API。

误区3:使用正则表达式完全过滤XSS。正解:XSS的变种极其繁多,正则难以穷尽且维护困难。对于普通文本输出,使用转义库;对于富文本,使用净化库。

误区4:认为使用了ORM框架就绝对安全。正解:JPA的@Query如果使用原生SQL并拼接字符串,同样危险。MyBatis的${}是危险的。必须坚持使用参数化查询。

安全自查清单:

  1. 输入验证:所有API接口是否都对入参使用了@Valid进行校验?校验规则是否足够严格(白名单)?
  2. 输出编码:所有从数据库取出或用户输入,最终要输出到HTML页面的数据,是否都经过了正确的上下文编码?模板引擎自动转义是否开启?
  3. SQL查询:代码中是否存在字符串拼接的SQL?MyBatis XML中是否使用了${}且参数来自用户输入?
  4. 富文本处理:如果需要富文本,是否使用了专业的HTML净化库?白名单是否最小化?
  5. 安全头:是否配置了CSP、X-Frame-Options等安全HTTP头?
  6. 依赖安全:是否定期扫描并升级存在已知漏洞的第三方依赖?
  7. 错误处理:应用是否返回了过于详细的数据库错误信息给前端?(这会给SQL注入攻击提供信息)
  8. 日志记录:是否记录了所有可疑的输入(如触发XSS过滤规则的请求)用于事后审计?

5.4 渗透测试与自动化扫描

除了自查,引入外部视角至关重要。

  • 使用ZAP或Burp Suite进行主动扫描:对应用进行自动化漏洞扫描,可以发现常见的XSS和SQL注入点。
  • 进行代码审计:使用SonarQube、Fortify等静态代码分析工具,可以识别出潜在的不安全代码模式。
  • 定期进行人工渗透测试:聘请专业的安全人员或让开发团队内部进行交叉测试,模拟真实攻击。

防护XSS和SQL注入是一场持久战,它贯穿于设计、编码、测试、部署和运维的全生命周期。在Spring Boot中,我们拥有强大的工具和生态来简化这项工作,但最关键的,始终是开发人员心中那根紧绷的“安全弦”。记住一个原则:永远不要信任用户输入的任何数据。将其作为所有安全实践的出发点,你的应用就成功了一大半。