【硬核】由XSS问题探索一下SpringMVC和FastJson
前言
好久没来写文章了,所以在某人的催促之下,把最近的一个需求进行总结一下。在这个需求的开发过程中,还是有不少收获的,所以做一下分享,如有补充与不正之处,欢迎大家的批评与斧正。
背景
我们先来讲一下这个需求的背景吧。看到这个标题,可以想到是不是因为系统遭遇了XSS攻击所以需要进行过滤。没错,这是因为前几天有一个憨憨提交了一段含有脚本的内容,结果成功保存进去,然后在展示的时候,浏览器直接alert出来这个提示(传统项目中的常见漏洞,见谅见谅),这个也是比较常见的XSS攻击的场景。为此,需要对XSS的内容进行过滤,以及对系统内的脏数据进行兼容处理(后来在系统里仔细搜了下,还确实有不少XSS的内容,估计是友商的友善操作)。
需求分析
看到这里,估计有不少小伙伴嗤之以鼻,心里在想:这个东西对我来说实在是太小儿科了。于是百度一下,啪的一声就把代码甩在我的脸上。这些答案,大致上是这样的:提供一个Xss的Filter,然后在这个Filter里面对Request进行RequestWrapper的封装,并且重写里面的getParameter方法和getParameterValues方法,然后对参数值进行Xss数据清洗。代码如下:
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
{
// 普通代码块
String temp = filterConfig.getInitParameter(PARAM_NAME_EXCLUSIONS);
String[] url = StringUtils.split(temp, DEFAULT_SEPARATOR_CHARS);
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}
}
log.info("WebFilter->[{}] init success...", filterConfig.getFilterName());
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (handleExcludeUrl(req, resp)) {
chain.doFilter(request, response);
return;
}
// 对HttpServletRequest进行包装
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response);
} else {
chain.doFilter(request, response);
}
}
/**
* 当前请求是否是排除的链接
*
* @return boolean
*/
private boolean handleExcludeUrl(HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(request, "HttpServletRequest must not be null");
Assert.notNull(response, "HttpServletResponse must not be null");
if (ObjectUtils.isEmpty(excludes)) {
return false;
}
// 返回除去host(域名或者ip)部分的路径
String requestUri = request.getRequestURI();
// 返回除去host和工程名部分的路径
String servletPath = request.getServletPath();
// 返回全路径
StringBuffer requestURL = request.getRequestURL();
for (String pattern : excludes) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(requestURL);
if (m.find()) {
return true;
}
}
return false;
}
@Override
public void destroy() {
}
/**
* 排除链接默认分隔符
*/
public static final String DEFAULT_SEPARATOR_CHARS = ",";
/**
* 需要忽略排除的链接参数名
*/
public static final String PARAM_NAME_EXCLUSIONS = "exclusions";
/**
* 需要忽略排除的链接参数值
* http://localhost,http://127.0.0.1,
*/
public static final String PARAM_VALUE_EXCLUSIONS = "";
/**
* 排除链接
*/
public List<String> excludes = Lists.newArrayList();
}
XssHttpServletRequestWrapper的代码如下
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.Enumeration;
/**
* XSS过滤处理
*
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* 默认XSS过滤处理白名单
*/
public final static Whitelist DEFAULT_WHITE_LIST = Whitelist.relaxed().addAttributes(":all", "style");
/**
* 唯一构造器
*
* @param request HttpServletRequest
*/
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getHeader(String name) {
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaders(String name) {
return super.getHeaders(name);
}
@Override
public String getRequestURI() {
return super.getRequestURI();
}
/**
* 需要手动调用此方法,SpringMVC默认应该使用getParameterValues来封装的Model
*/
@Override
public String getParameter(String name) {
String parameter = super.getParameter(name);
if (parameter != null) {
return cleanXSS(parameter);
}
return null;
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapeValues = new String[length];
for (int i = 0; i < length; i++) {
if (null != values[i]) {
// 防xss攻击和过滤前后空格
escapeValues[i] = cleanXSS(values[i]);
} else {
escapeValues[i] = null;
}
}
return escapeValues;
}
return null;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return super.getInputStream();
}
public static String cleanXSS(String value) {
if (value != null) {
value = StringUtils.trim(Jsoup.clean(value, DEFAULT_WHITE_LIST));
}
return value;
}
}
当然,这里我就使用了Jsoup作为XSS内容清除的工具,具体的用法,各位可以自行百度。
我想以上应该是各位比较常见的写法。这样的写法当然没啥问题,但是其实有一个局限性,那就是这种方式只适合form表单提交的请求,而如果是那种JSON数据请求的,就获取不到数据,而我们系统大多数的接口请求都是JSON数据提交的。
此外还有一个问题,那就是我们系统里面已经有XSS脏数据了,那么要解决这个问题,也是两个角度出发:
-
处理系统当中的脏数据,也就是SQL清洗. 2.在数据返回的时候进行数据清洗.
对于第一种方案,由于系统是分库分表,而且期间的业务表冗余庞大,SQL的清除工作会显得十分困难,而且存在巨大的风险与隐患。所以我们这边还是采用第二种方案,由于数据都是采用JSON数据返回,那么只需要做一个通用的转换,进行数据清洗即可。
那么我们对需要做的事情进行数据总结一下:
-
对于JSON数据请求进行数据清洗 -
对于JSON数据返回进行数据清洗
源码探索
按照我以往设计解决方案的时候,我都会先从执行流程以及源码等方面来看这些问题。所以我想对于JSON的请求和返回的过程进行的探索可能会有所启示。目前大多数系统都是基于SpringMVC作为前置框架,所以以下也是基于SpringMVC的源码进行解析,spring-webmvc的版本是5.2.9.RELEASE,其他版本的代码应该大致相同,就不做过多细究。在JSON数据上我们使用的是fastjson(虽然fastjson在稳定性等方面还是不如jackson,但是我希望我们国产的能够越来越好,还是要支持一波),使用的版本是1.2.72。
JSON数据返回
首先我们还是先从JSON数据返回开始着手,这个也是因为,即便无法完成对JSON数据请求的数据清洗,能够从JSON数据返回当中解决,进行一个兜底的方案处理,至少还是一个让人安心的事情。
SpringMVC在扫描到@ResponseBody之后,就知道这个接口是用来返回JSON数据的。在spring配置文件中,我们配置了如下:
<bean id="fastJsonHttpMessageConverter"
class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
<property name="features">
<array>
<value>DisableCircularReferenceDetect</value>
</array>
</property>
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="fastJsonHttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
当然各位也可以使用注解的方式
@Configuration
public class SpringWebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
// 避免循环引用
SerializerFeature.DisableCircularReferenceDetect);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(Charset.forName("UTF-8"));
List<MediaType> mediaTypeList = new ArrayList<>();
mediaTypeList.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypeList);
// 为什么不是直接add,这个在下文中会提到
converters.set(0, converter);
}
}
两种方式其实都是将自定义的HttpMessageConverter注入到Spring内部当中的messageConverts当中,效果是一样的。
DispatcherServlet
对于这个类,我想我们应该是再熟悉不过了,即便没有看过源码。各位在刚学SpringMVC的时候,各类教程里面都是告诉你需要在web.xml配置文件当中配置这个类。这是因为DispatcherServlet这个类是作为SpringMVC所有的请求的一个入口类。当Tomcat将请求包装之后流转到HttpServlet当中,这个时候的HttpServlet实际上是Spring提供的FrameworkServlet,紧接着进入的doService方法则是由FrameworkServlet,也就是DispatcherServlet所实现。如图所示:那么在这个doDispatch方法里面,经过一层层调用,就会调用到HandlerMethodReturnValueHandlerComposite#handleReturnValue(中间的环节我就略过了,各位也可以自行DEBUG)
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
HandlerMethodReturnValueHandlerComposite
这里有一个selectHandler私有方法:
private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
SpringMVC里面内置了15个HandlerMethodReturnValueHandler,每一个HandlerMethodReturnValueHandler都实现了supportsReturnType。通过这个接口我们来确定在运行的时候是用哪一个处理器。这里我们来看下RequestResponseBodyMethodProcessor所实现的supportsReturnType方法:
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
可以看到,这里还是围绕着@ResponseBody这个注解来的。这下,以后面试官要是问你,为什么@ResponseBody是用来返回JSON数据的,那么,看到这个步骤,我们大概清楚,因为这个注解的存在,所以我们所使用的HandlerMethodReturnValueHandler选择的是RequestResponseBodyMethodProcessor。
既然确定了具体的处理器,那么我们就进入到它的handleReturnValue方法。
RequestResponseBodyMethodProcessor
直接先看源码
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
这里其实也没什么好说的,就是直接调用了它所继承的抽象方法。
AbstractMessageConverterMethodProcessor
进入到writeWithMessageConverters这个方法,我看来看到这其中的这一部分
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
// 写入之前执行动作
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
// 具体的messageConverter进行写入
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
}
看上去这段代码好像很麻烦,但是实际上关键的就两个步骤:
-
getAdvice().beforeBodyWrite() -
write() 没错,write方法就是我们返回JSON数据写入IO流,而getAdvice().beforeBodyWrite()就是在写入之前可以对数据进行处理的前置方法,也是Spring给开发者留下的钩子。所以这就可能是一个可以考虑的地方,在下面还会提到,我们继续往下看。
FastJsonHttpMessageConverter
我们继续回到上面的for循环当中,看看这个this.messageConverters
为什么是FastJsonHttpMessageConverter
我们可以看到,SpringMVC内置了很多HttpMessageConverter。this.messageConverters是在Spring容器启动的时候就会进行加载设置,先加载系统内置的,最后才会加载我们自定义的,也就是我们的主角FastJsonHttpMessageConverter。这些知道为什么我要写成
converters.set(0, converter);
目的就是为了在遍历的时候首先使用FastJsonHttpMessageConverter,不然默认执行的就是MappingJackson2HttpMessageConverter。(希望有朝一日,FastJsonHttpMessageConverter也能成为内置的转换器)
快说说this.messageConverters是怎么加载的
好吧,上面我一笔带过这个流程,想偷个懒,可还是被你们的好奇心发现了。上面说到,this.messageConverters是在Spring容器启动的时候就会进行加载设置,先加载系统内置的,最后才会加载我们自定义的。那它是怎么加载的呢?
我们先来DUBUG一下,从结果往前推。看看这里的堆栈
额,这样的解释我猜你们显然是不尽兴的,那么我们继续往前推,看看是哪里的程序进入到这里: 我想看到这里,总该明白,这个this.delegates列表里面有两个WebMvcConfigurer的实现类,按照顺序进行遍历。可以看到我们自定义的WebMvcConfigurer是晚于Spring内置的WebMvcAutoConfigurationAdapter。
额,不是吧,竟然还会问为什么this.delegates里面的顺序是这样的?
那么我们看下上面是怎么给this.delegates进行赋值的 可以看到这个WebMvcConfigurer也是由外部传入的,我们看下是哪里传入的 我想这个写法就应该很明白了,根据Autowired将容器中的WebMvcConfigurer赋值到这个configurers当中,至于赋值过程中的顺序,则会参考是不是有@Order注解,我们可以看到Spring给我们提供的WebMvcAutoConfigurationAdapter上面写了@Order(0)。所以各位老板要是不想使用
converters.set(0, converter);
这种写法而是直接add,那么可以在自定义的WebMvcConfigurer将优先级早于WebMvcAutoConfigurationAdapter也是可以的。
write做了什么事情呢
既然上面已经解释了为什么是FastJsonHttpMessageConverter,那么接下来我们就可以来看它的write方法。
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
ByteArrayOutputStream outnew = new ByteArrayOutputStream();
try {
// ... 省略
int len = JSON.writeJSONString(outnew, //
fastJsonConfig.getCharset(), //
value, //
fastJsonConfig.getSerializeConfig(), //
//fastJsonConfig.getSerializeFilters(), //
allFilters.toArray(new SerializeFilter[allFilters.size()]),
fastJsonConfig.getDateFormat(), //
JSON.DEFAULT_GENERATE_FEATURE, //
fastJsonConfig.getSerializerFeatures());
// ... 省略
} catch (JSONException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
} finally {
outnew.close();
}
}
省略一些中间环节,最后它就是进行核心的JSON化操作。接下来就是看Fastjson在这个过程中是否给我们预留了上面类似的钩子。答应是肯定的,毕竟也是这么优秀的开源项目。直接上代码吧
public class JavaBeanSerializer extends SerializeFilterable implements ObjectSerializer {
protected void write(JSONSerializer serializer, //
Object object, //
Object fieldName, //
Type fieldType, //
int features,
boolean unwrapped
) throws IOException {
// 这里就是处理每一个key-value的过程
Object originalValue = propertyValue;
propertyValue = this.processValue(serializer, fieldSerializer.fieldContext, object, fieldInfoName, propertyValue, features);
}
protected Object processValue(JSONSerializer jsonBeanDeser, //
BeanContext beanContext,
Object object, //
String key, //
Object propertyValue, //
int features) {
if (jsonBeanDeser.valueFilters != null) {
for (ValueFilter valueFilter : jsonBeanDeser.valueFilters) {
propertyValue = valueFilter.process(object, key, propertyValue);
}
}
}
}
在Fastjson处理每一个key-value的时候,都会去执行这个valueFilters。所以我们只需要在配置FastJsonHttpMessageConverter的时候,带上我们自定义的ValueFilter,这样它就会进行处理。我们来看下这个接口
public interface ValueFilter extends SerializeFilter {
Object process(Object object, String name, Object value);
}
我们可以看到这个接口里面有一个process方法,所以我们也可以利用这个钩子,在输出JSON数据之前,对内容进行清除修改。
那么怎么选择呢
那么综合来看我们可以发现,整个流程当中会提到两个钩子
-
SpringMVC写入之前的钩子 -
SpringMVC使用的Fastjson在写入JSON之前的钩子 那各位觉得应该要使用哪一个钩子呢?我个人觉得,是使用Fastjson的钩子。因为假如是用SpringMVC提供的钩子,当拿到对象之后,大概率是会使用反射的方式进行修改内容,而后续的序列化,不管是Fastjson还是Jackson也是会用到反射。这两轮的反射会对性能造成一定程度的影响,所以与其都要修改,不如就留在Fastjson的钩子中去处理。
JSON数据请求
说完了返回的这一层面,我们再来看看JSON数据请求的这一情况,能不能也有相关的解决方案。既然说到请求,那么我们还是要回归到上面我们讲的DispatcherServlet。
DispatcherServlet
又回到这个类,这里我们就从doService入手,紧接着调用了doDispatch方法。(什么,你连为什么会走入doService都不知道,我……,出门左转找下Tomcat源码攻略)
/**
* Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
* for the actual dispatching.
*/
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 一些配置操作,这里忽略
try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
在这个doDispatch方法里面呢,会去寻找对应的HandlerAdapter,也就是RequestMappingHandlerAdapter这个适配器。也就是上面图片中的第二个小方框。至于第一个,就是大家经常背的面试题,问SpringMVC请求处理流程,其中获取接口处理器就是在这个地方。当然这里只是提一下。
那么是怎么决定是RequestMappingHandlerAdapter呢,我们看下这个getHandlerAdapter方法可以看到,Spring容器内提供了4个内置的handlerAdapters。RequestMappingHandlerAdapter并没有重写这个supports方法,所以使用的是抽象方法AbstractHandlerMethodAdapter当中的supports方法
/**
* This implementation expects the handler to be an {@link HandlerMethod}.
* @param handler the handler instance to check
* @return whether or not this adapter can adapt the given handler
*/
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
RequestMappingHandlerAdapter重写了supportsInternal方法,恒等于true
/**
* Always return {@code true} since any method argument and return value
* type will be processed in some way. A method argument not recognized
* by any HandlerMethodArgumentResolver is interpreted as a request parameter
* if it is a simple type, or as a model attribute otherwise. A return value
* not recognized by any HandlerMethodReturnValueHandler will be interpreted
* as a model attribute.
*/
@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
return true;
}
大家也可以看看上面的英文注释,还是比较易懂的。
而前者,在封装的Http请求的时候本就是使用HandlerMethod作为其中的处理类。所以是结合这些因素就决定是RequestMappingHandlerAdapter作为适配器。
InvocableHandlerMethod
画风一转,我们来到了这个类。在确定RequestMappingHandlerAdapter作为处理类适配器之后,紧接着会进入一系列操作。其他操作我们在这里不做关心,因为我们关心的是对于参数部分的操作。这里的getMethodArgumentValues方法就是用来获取接口中的参数而具体的是由this.resolvers调用resolveArgument这个方法来处理
HandlerMethodArgumentResolverComposite
上面的this.resolvers就是这个类来着这里又需要进行getArgumentResolver来获取真正的参数处理器HandlerMethodArgumentResolver这里的缓存我就不多说了。系统里面提供了26个参数处理器。
RequestResponseBodyMethodProcessor
我们在平常的开发当中,如果前端页面传的是一个json数据的话,我们会在SpringMVC的参数对象前面加上一个@RequestBody。以前学的时候,包括做的时候,反正只要加上这个注解,就可以接收到JSON数据了。那么今天我们终于可以知道,也正是因为有了这个注解的规范,流程上会根据supportsParameter接口方法来定位到这个处理器。(划重点,划重点,划重点)进入到resolveArgument,我们又看到了熟悉的东西。没错,这里的readWithMessageConverters又用到了我们之前说的MessageConverters。进入这个方法,看看它的实现
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// 之前的逻辑省略
EmptyBodyCheckingHttpInputMessage message;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// 省略后置逻辑
return body;
}
可以看到这个里面的写法,跟之前的写法如出一辙,只是相比于返回JSON数据,多了一步后置处理。
那么由此我们也可以推测这里也存在两个钩子:
-
SpringMVC提供的前置处理或者后置处理钩子处理数据 -
HttpMessageConverter提供的钩子处理
FastJsonHttpMessageConverter
又回到这个类。上面写到的是写入JSON,这里是从IO中读取JSON接下来就又回到我们平时使用的JSON.parseObject()方法了。这里提供了很多的配置,那么这里是否会有我们可能会用到的钩子呢。
这个答案是显然。跟着程序的执行,我们来到了Fastjson解析比较核心的部分这一部分是从IO流中解析成字符串,紧接着调用parseObject这里提供了三种类型的processer,我也可以理解为是三种过滤器。很可惜这三种过滤器只有在找不到匹配的属性的时候才会进行调用。
那看来从Fastjson身上寻找钩子的方案可能行不通(当然,可能存在其他我不知道的钩子,只是按照我现在看下来可能有点鸡肋)
getAdvice()
既然Fastjson这条路可能行不通,那么我们还是来看下SpringMVC提供的钩子吧。
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
这里的getAdvice()获取的类型是RequestResponseBodyAdviceChain,它是AbstractMessageConverterMethodArgumentResolver当中的属性,所以我们只要关注下它是如何被赋值的(总感觉又要被你们坑了)。
从源码来看,是在构造函数里面进行赋值的,那么接着看看是从哪里进行传入参数的
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
尤其是上面的这个注释,似乎这个地方有我们想要的这个方法里面,首先是寻找系统中的ControllerAdviceBean,寻找的依据还是根据这个ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
/**
* Find beans annotated with {@link ControllerAdvice @ControllerAdvice} in the
* given {@link ApplicationContext} and wrap them as {@code ControllerAdviceBean}
* instances.
* <p>As of Spring Framework 5.2, the {@code ControllerAdviceBean} instances
* in the returned list are sorted using {@link OrderComparator#sort(List)}.
* @see #getOrder()
* @see OrderComparator
* @see Ordered
*/
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
if (!ScopedProxyUtils.isScopedTarget(name)) {
ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class);
if (controllerAdvice != null) {
// Use the @ControllerAdvice annotation found by findAnnotationOnBean()
// in order to avoid a subsequent lookup of the same annotation.
adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice));
}
}
}
OrderComparator.sort(adviceBeans);
return adviceBeans;
}
可以看到这里是说,主要是根据@ControllerAdvice这个注解来定位到这个类,并且封装成ControllerAdviceBean对象。
我们再回到上面那个图,如果是实现了RequestBodyAdvice或者ResponseBodyAdvice接口的类,就会加入到requestResponseBodyAdviceBeans这个列表当中,并且会将这个Advice复制到我们上面说的this.requestResponseBodyAdvice,并且,是放在首位!可见,如果我们实现了自定义的Advice,Spring是希望优先执行我们的Advice。此外,我们可以看到这里也有ResponseBodyAdvice,没错,在上面没有选用的JSON数据返回所涉及到的钩子,也是在这个地方进行赋值的。
为此,我们可以简单实现下这一Advice
@RestControllerAdvice
public class XssRequestControllerAdvice implements RequestBodyAdvice {
/**
* 默认XSS过滤处理白名单
*/
public final static Whitelist DEFAULT_WHITE_LIST = Whitelist.relaxed().addAttributes(":all", "style");
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(RequestBody.class);
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
String bodyStr = IOUtils.toString(inputMessage.getBody(),"utf-8");
if (StringUtils.isNotEmpty(bodyStr)) {
bodyStr = StringUtils.trim(Jsoup.clean(bodyStr, DEFAULT_WHITE_LIST));
}
return IOUtils.toInputStream(bodyStr,"utf-8");
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
这边我重写了beforeBodyRead。从IO中读取字符串,在进行XSS数据清洗之后,并且再输出成IO流。虽然,在read部分又会进行一次IO流的读取。
小结
通过上面的一系列源码探索(这波探索真TM多),主要是针对JSON数据的XSS过滤方案的探索与构思。最后这个需求我做了如下的方案:
-
提供XssFilter进行XSS的数据过滤,这个是为了应对form表单提交的请求方式 -
提供XssRequestControllerAdvice和FastJsonXssValueFilter分别对请求和相应中的数据进行XSS过滤,这个是为了应对JSON数据的请求方式,前者可以防止后来的XSS数据写入,后者可以过滤已经存在系统当中的XSS脏数据,并且用户进行保存操作就可以纠正XSS数据。