Spring MVC 请求参数全局拦截器篡改的排查与规范方案
大家好,今天我们来聊聊 Spring MVC 中请求参数被全局拦截器篡改的问题。这是一个在实际开发中容易遇到,但又比较隐蔽的 bug。处理不当,会导致数据安全隐患,业务逻辑混乱,甚至造成难以追踪的错误。
一、问题场景描述
在 Spring MVC 应用中,我们经常使用拦截器(Interceptor)来处理一些通用的请求逻辑,例如:
- 权限校验
- 日志记录
- 统一参数处理
- 防止 XSS 攻击
通常,我们会继承 HandlerInterceptor 接口,并实现 preHandle、postHandle 和 afterCompletion 方法。问题往往出现在 preHandle 方法中,因为这个方法在请求到达 Controller 之前执行,有机会修改请求参数。
假设我们有一个需求:为了防止恶意用户提交包含 HTML 标签的数据,我们需要在请求到达 Controller 之前,对所有 String 类型的参数进行 HTML 编码。于是我们编写了一个拦截器:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
public class XSSFilterInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String parameterName = parameterNames.nextElement();
String[] parameterValues = request.getParameterValues(parameterName);
if (parameterValues != null) {
for (int i = 0; i < parameterValues.length; i++) {
if (parameterValues[i] instanceof String) {
parameterValues[i] = cleanXSS(parameterValues[i]); // 使用自定义的cleanXSS方法进行HTML编码
//关键代码:直接修改 request 的 parameterValues
request.setAttribute(parameterName, parameterValues[i]);
}
}
}
}
return true;
}
private String cleanXSS(String value) {
// 这里是 HTML 编码的实现,为了简洁,省略具体代码
return org.springframework.web.util.HtmlUtils.htmlEscape(value);
}
}
并将拦截器配置到 Spring MVC 中:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new XSSFilterInterceptor())
.addPathPatterns("/**"); // 拦截所有请求
}
}
看起来一切都很完美,所有请求都会经过 XSS 过滤。但是,这种做法存在一个严重的问题:直接修改 request.setAttribute 中的参数值,可能会导致 Controller 中获取到的参数与用户实际提交的参数不一致。尤其是在使用 ModelAttribute 注解或者通过反射获取参数值的情况下,更容易出现问题。
二、问题排查方法
当出现请求参数被篡改的现象时,我们可以按照以下步骤进行排查:
-
确认问题范围: 首先,确认问题是否只出现在特定的 Controller 或者特定的请求中。如果是,可以缩小排查范围,重点关注相关的拦截器和 Controller 代码。
-
禁用拦截器: 临时禁用所有拦截器,观察问题是否仍然存在。如果问题消失,说明问题确实是由某个拦截器引起的。
-
逐个启用拦截器: 逐个启用拦截器,每次启用一个,观察问题是否出现。这样可以快速定位到导致问题的拦截器。
-
断点调试: 在拦截器的
preHandle方法中设置断点,观察请求参数的值在拦截器执行前后是否发生了变化。 -
日志记录: 在拦截器的
preHandle方法中添加日志记录,记录请求参数的值,方便后续分析。 -
对比参数获取方式: 在Controller中,使用不同的方式获取参数(例如:
@RequestParam,@ModelAttribute,request.getParameter())并打印出来,对比它们的值是否一致。这有助于判断是哪种参数获取方式受到了拦截器的影响。
代码示例:Controller参数获取对比
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@Controller
public class TestController {
@GetMapping("/test")
@ResponseBody
public String test(
@RequestParam("name") String name,
@ModelAttribute("user") User user,
HttpServletRequest request
) {
System.out.println("@RequestParam: " + name);
System.out.println("@ModelAttribute: " + user.getName());
System.out.println("request.getParameter("name"): " + request.getParameter("name"));
return "OK";
}
//ModelAttribute 对应的实体类
@ModelAttribute("user")
public User getUser(@RequestParam("name") String name) {
User user = new User();
user.setName(name);
return user;
}
}
class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
表格总结排查步骤:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 确认问题范围 | 缩小排查范围,确定受影响的 Controller 和请求 |
| 2 | 禁用所有拦截器 | 问题消失,确认是由拦截器引起 |
| 3 | 逐个启用拦截器 | 定位到导致问题的拦截器 |
| 4 | 断点调试拦截器 preHandle 方法 |
观察参数值变化 |
| 5 | 添加日志记录 | 方便后续分析 |
| 6 | 对比Controller参数获取方式 | 确定哪种参数获取方式受到影响 |
三、规范方案
为了避免请求参数被篡改的问题,我们需要采取一些规范措施。
-
避免直接修改 Request 参数: 拦截器的主要职责是对请求进行预处理,而不是直接修改请求参数。如果需要修改参数,应该使用更安全的方式。
-
使用 Request Wrapper: 可以创建一个
HttpServletRequestWrapper,在 Wrapper 中对参数进行修改,然后将 Wrapper 传递给后续的 Handler。这样可以保证原始的 Request 对象不被修改。
代码示例:使用 Request Wrapper
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.HashMap;
import java.util.Map;
public class XSSRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> filteredParams = new HashMap<>();
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
// 预先处理所有参数
Map<String, String[]> originalParams = request.getParameterMap();
for (String paramName : originalParams.keySet()) {
String[] originalValues = originalParams.get(paramName);
String[] filteredValues = new String[originalValues.length];
for (int i = 0; i < originalValues.length; i++) {
filteredValues[i] = cleanXSS(originalValues[i]);
}
filteredParams.put(paramName, filteredValues);
}
}
@Override
public String getParameter(String name) {
String[] values = filteredParams.get(name);
if (values != null && values.length > 0) {
return values[0];
}
return null;
}
@Override
public String[] getParameterValues(String name) {
return filteredParams.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return filteredParams;
}
private String cleanXSS(String value) {
// 这里是 HTML 编码的实现,为了简洁,省略具体代码
return org.springframework.web.util.HtmlUtils.htmlEscape(value);
}
}
修改拦截器,使用 Request Wrapper:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class XSSFilterInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
XSSRequestWrapper xssRequest = new XSSRequestWrapper(request);
// 将包装后的 request 传递给后续的 Handler
return true;
}
}
注意:
- 需要配置
Filter来使用XSSRequestWrapper,因为拦截器无法直接修改request对象。 Filter需要在拦截器之前执行,以确保拦截器获取到的是包装后的请求对象。
Filter配置示例:
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<Filter> xssFilterRegistrationBean() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XSSFilter());
registration.addUrlPatterns("/*");
registration.setName("xssFilter");
registration.setOrder(1); // 设置优先级,确保Filter在拦截器之前执行
return registration;
}
}
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class XSSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
XSSRequestWrapper xssRequest = new XSSRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}
}
-
使用 AOP: 如果只需要对特定的 Controller 方法进行参数处理,可以使用 AOP,在方法执行前后对参数进行修改。
-
统一参数处理类: 可以创建一个专门的参数处理类,负责对所有请求参数进行统一处理。然后在 Controller 中调用这个类的方法来获取参数,避免直接从 Request 对象中获取。
-
自定义参数解析器: Spring MVC 提供了自定义参数解析器的机制,可以通过实现
HandlerMethodArgumentResolver接口,自定义参数解析逻辑。这样可以更好地控制参数的解析过程,避免参数被篡改。
代码示例:自定义参数解析器
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class XSSStringArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(String.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String parameterName = parameter.getParameterName();
String parameterValue = webRequest.getParameter(parameterName);
if (parameterValue != null) {
return cleanXSS(parameterValue);
}
return null;
}
private String cleanXSS(String value) {
// 这里是 HTML 编码的实现,为了简洁,省略具体代码
return org.springframework.web.util.HtmlUtils.htmlEscape(value);
}
}
配置自定义参数解析器:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new XSSStringArgumentResolver());
}
}
表格总结规范方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Request Wrapper | 避免直接修改 Request 对象,安全可靠 | 需要配置 Filter,实现较为复杂 | 需要对所有请求参数进行统一处理 |
| AOP | 灵活,只对特定方法生效 | 代码侵入性较强 | 只需要对特定的 Controller 方法进行参数处理 |
| 统一参数处理类 | 代码复用性高 | 需要手动调用,容易遗漏 | 需要对多个 Controller 方法进行相同的参数处理 |
| 自定义参数解析器 | Spring MVC 原生支持,控制参数解析过程 | 实现较为复杂,需要了解 Spring MVC 的参数解析机制 | 需要对特定类型的参数进行特殊处理 |
四、最佳实践
在实际开发中,建议采用以下最佳实践:
-
明确拦截器的职责: 拦截器的主要职责是对请求进行预处理,例如权限校验、日志记录等。尽量避免在拦截器中修改请求参数。
-
选择合适的参数处理方案: 根据实际需求,选择合适的参数处理方案。如果需要对所有请求参数进行统一处理,可以使用 Request Wrapper。如果只需要对特定的 Controller 方法进行参数处理,可以使用 AOP 或者自定义参数解析器。
-
编写单元测试: 针对拦截器和参数处理逻辑编写单元测试,确保代码的正确性和稳定性。
-
代码审查: 定期进行代码审查,及时发现潜在的问题。
-
记录操作日志: 记录用户的操作日志,方便问题排查和安全审计。
五、案例分析
假设一个电商网站,用户可以提交商品评论。为了防止恶意用户提交包含 JavaScript 代码的评论,我们需要对评论内容进行 XSS 过滤。
- 错误的做法: 在拦截器中直接修改
request.setAttribute中的评论内容。 - 正确的做法: 使用 Request Wrapper,在 Wrapper 中对评论内容进行 HTML 编码,然后将 Wrapper 传递给 Controller。或者使用自定义参数解析器,在解析评论内容时进行 HTML 编码。
六、总结与建议
总而言之,在 Spring MVC 中,全局拦截器篡改请求参数是一个需要重视的问题。通过合理的排查方法和规范方案,我们可以有效地避免这个问题,提高应用程序的安全性和稳定性。关键在于:
- 理解拦截器的职责,避免过度干预请求参数。
- 选择合适的参数处理方案,根据实际需求进行权衡。
- 加强代码审查和单元测试,确保代码质量。
希望今天的分享对大家有所帮助。谢谢!