preface

I haven’t written an article for a long time, so AT someone’s urging, I summarize a recent requirement. In the development process of this demand, there are still a lot of harvest, so do share, if there is a supplement and wrong place, welcome everyone’s criticism and workmanship.

background

Let’s start with the background to this requirement. Looking at the title, you can assume that filtering is needed because the system is experiencing an XSS attack. Yes, this is because a few days ago, a bunch of people submitted a piece of content containing script, the result successfully saved, and then during the display, the browser directly alert this prompt (common vulnerability in traditional projects, sorry for your mistake), this is also a relatively common XSS attack scenario. To this end, the content of XSS needs to be filtered, as well as the dirty data in the system for compatible processing (later in the system carefully searched, there are indeed a lot of XSS content, it is estimated that the friendly operation of friends).

Demand analysis

See here, it is estimated that many partners sniffed, thinking: this thing for me is really too pediatric. So I googled it and slapped it in my face. The answers, roughly, look like this: Provide a RequestWrapper for the Request in the Xss Filter, override getParameter and getParameterValues, and perform Xss data cleaning on the parameter values. The code is as follows:

public class XssFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");
        {
            // Plain code block
            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;
            }
            // Wrap HttpServletRequest
            XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
            chain.doFilter(xssRequest, response);
        } else{ chain.doFilter(request, response); }}/** * whether the current request is an excluded link **@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;
        }
        // Return the path excluding the host (domain name or IP) part
        String requestUri = request.getRequestURI();
        // Return the path without the host and project name parts
        String servletPath = request.getServletPath();
        // Return to full path
        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(a) {}/** * excludes the link default separator */
    public static final String DEFAULT_SEPARATOR_CHARS = ",";

    /** * The name of the link parameter that needs to be ignored */
    public static final String PARAM_NAME_EXCLUSIONS = "exclusions";

    / * * * need to ignore rule out link parameter values * http://localhost, http://127.0.0.1, * /
    public static final String PARAM_VALUE_EXCLUSIONS = "";

    /** * exclude links */
    public List<String> excludes = Lists.newArrayList();

}
Copy the code

XssHttpServletRequestWrapper code is as follows

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 {

    /** * Default XSS filter handles whitelist */
    public final static Whitelist DEFAULT_WHITE_LIST = Whitelist.relaxed().addAttributes(":all"."style");

    /** * unique constructor **@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(a) {
        return super.getRequestURI();
    }

    /** * This method needs to be called manually. SpringMVC should use getParameterValues to encapsulate Model */ by default
    @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]) {// Prevent XSS attacks and filter Spaces before and after
                    escapeValues[i] = cleanXSS(values[i]);
                } else {
                    escapeValues[i] = null; }}return escapeValues;
        }
        return null;
    }

    @Override
    public ServletInputStream getInputStream(a) throws IOException {
        return super.getInputStream();
    }
    

    public static String cleanXSS(String value) {
        if(value ! =null) {
            value = StringUtils.trim(Jsoup.clean(value, DEFAULT_WHITE_LIST));
        }
        returnvalue; }}Copy the code

Of course, HERE I use Jsoup as the XSS content clearing tool, the specific usage, you can baidu.



I think the above should be more common for you to write. There is nothing wrong with this, but there is a limitation: this method is only suitable for form submission requests, and if it is a JSON data request, it will not get the data, and most of our system interface requests are JSON data submissions.

In addition, there is another problem, that is, our system already has dirty XSS data, so to solve this problem, also from two perspectives:

  1. Handle dirty data in the system, also known as SQL cleaning.

2. Clean the data when the data is returned.

For the first scheme, because the system is divided into database and table, and the business table during the huge redundancy, SQL clearing work is very difficult, and there are huge risks and hidden dangers. Therefore, we still use the second scheme. Since all the data are returned using JSON data, we only need to do a general transformation to clean the data. So let’s make a data summary of what we need to do:

  1. Data cleaning for JSON data requests
  2. Data cleaning for JSON data returns

The source code to explore

When I design solutions in the past, I always look at these problems in terms of execution flow and source code. So I thought an exploration of the JSON request and return process might be instructive. At present, most systems are based on SpringMVC as the front-end framework, so the following is also based on SpringMVC source code parsing, the version of Spring-WebMVC is 5.2.9.RELEASE, other versions of the code should be roughly the same, without too much detail. For JSON data, we use Fastjson (although Fastjson is not as good as Jackson in terms of stability, but I hope our domestic product can get better and better, we still need to support a wave), and the version used is 1.2.72.

JSON data return

First, we will start with JSON data return. This is also because, even if it is not possible to complete the data cleaning of JSON data requests, it is at least a reassuring thing to be able to solve the problem from JSON data return. SpringMVC, after scanning @responseBody, knows that this interface is used to return JSON data. In the Spring configuration file, we configured the following:

<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>
Copy the code

Of course, you can also use annotations

@Configuration
public class SpringWebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List
       
        > converters)
       > {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();
        config.setSerializerFeatures(
                // Avoid circular references
                SerializerFeature.DisableCircularReferenceDetect);
        converter.setFastJsonConfig(config);
        converter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> mediaTypeList = new ArrayList<>();
        mediaTypeList.add(MediaType.APPLICATION_JSON);
        converter.setSupportedMediaTypes(mediaTypeList);
        // Why not just add, which will be mentioned later
        converters.set(0, converter); }}Copy the code

Both methods are more like injecting your own custom HttpMessageConverter into the Messageconverter verts of Spring internals, with the same effect.

DispatcherServlet

For this class, I think we should be very familiar with, even without looking at the source. When you first learned SpringMVC, you were told in various tutorials that you needed to configure this class in your web.xml configuration file. This is because the DispatcherServlet class acts as an entry class for all SpringMVC requests. When Tomcat wraps the request into an HttpServlet, the HttpServlet is actually the FrameworkServlet provided by Spring, and the doService method that enters it is the FrameworkServlet. That’s what DispatcherServlet does. As shown in the figure:Layers in the doDispatch method, then, after a call, will call to HandlerMethodReturnValueHandlerComposite# handleReturnValue (I can skip the intermediate link, you can 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);
	}
Copy the code

HandlerMethodReturnValueHandlerComposite

Here is a selectHandler private method:

	private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
		boolean isAsyncValue = isAsyncReturnValue(value, returnType);
		for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
			if(isAsyncValue && ! (handlerinstanceof AsyncHandlerMethodReturnValueHandler)) {
				continue;
			}
			if (handler.supportsReturnType(returnType)) {
				returnhandler; }}return null;
	}
Copy the code

For SpringMVC built in 15 HandlerMethodReturnValueHandler inside, each HandlerMethodReturnValueHandler supportsReturnType is realized. This interface is used to determine which processor to use at runtime. Here we see the implementation of the RequestResponseBodyMethodProcessor supportsReturnType method:

	@Override
	public boolean supportsReturnType(MethodParameter returnType) {
		return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
				returnType.hasMethodAnnotation(ResponseBody.class));
	}
Copy the code

And as you can see, it’s still around the @responsebody annotation. So if you’re asked, in the future, why is @responseBody used to return JSON data, then, as we can see from this step, because of this annotation, So we use HandlerMethodReturnValueHandler RequestResponseBodyMethodProcessor is selected. Now that we’ve identified the specific handler, let’s go to its handleReturnValue method.

RequestResponseBodyMethodProcessor

Look directly at the source code

	@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);
	}
Copy the code

In fact, there is nothing to say here, but it directly calls the abstract method it inherits.

AbstractMessageConverterMethodProcessor

Enter the writeWithMessageConverters this method, it seems to me to see this part of it

if(selectedMediaType ! =null) {
    selectedMediaType = selectedMediaType.removeQualityValue();
    for(HttpMessageConverter<? > messageConverter :this.messageConverters) {
        if (messageConverter instanceof GenericHttpMessageConverter) {
            if (((GenericHttpMessageConverter) messageConverter).canWrite(
                    declaredType, valueType, selectedMediaType)) {
                // Perform the action before writingoutputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<? >>) messageConverter.getClass(), inputMessage, outputMessage);if(outputValue ! =null) {
                    addContentDispositionHeader(inputMessage, outputMessage);
                    // Write to 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; }}}Copy the code

This code may seem like a lot of work, but there are only two key steps:

  1. getAdvice().beforeBodyWrite()
  2. write()

Yes, the write method is where we return JSON data to write to the IO stream, while getAdvice().beforeBodyWrite() is a preloading method that processes data before writing, and is a hook that Spring leaves behind for developers. So that might be a place to think about, and we’ll talk about that a little bit more, and we’ll move on.

FastJsonHttpMessageConverter

Let’s go back to the for loop above and look at this. MessageConverters

Why is FastJsonHttpMessageConverter

As you can see, SpringMVC has a lot of HttpMessageconverters built in. This messageConverters is started in the Spring container will be loaded when setting, built-in first loading system, the last to be loaded our custom, also is our protagonist FastJsonHttpMessageConverter. Do you know why I wrote it

converters.set(0, converter);
Copy the code

The purpose is to in the first to use FastJsonHttpMessageConverter traversal, otherwise the default execution is MappingJackson2HttpMessageConverter. (I hope one day, FastJsonHttpMessageConverter can also become a built-in converter)

How is this. MessageConverters loaded

Okay, SO I skimmed through the process to get a little lazy, but your curiosity caught up with me. This. MessageConverters is loaded when the Spring container is started. The built-in Settings are loaded first and the custom ones are loaded last. So how does it load?

So let’s DUBUG it and work backwards from the results. Look at the stack hereAs you can see, the list of converters passed in already has 10 built-in converters, so this is the assignment system built-in converters, and then it is our custom converters.

Well, I guess you’re not happy with that, so let’s go ahead and see where the program enters here:If I were to look at this, there would be this. Delegates list there are two implementation classes of WebMvcConfigurer traversed in sequence. Can see our custom WebMvcConfigurer is later than the Spring built-in WebMvcAutoConfigurationAdapter.

Oh, god, why is this.delegates in this order?

So how do we assign this.delegates up hereYou can see that WebMvcConfigurer is also coming in from outside, so where is it coming inAutowired assigns the WebMvcConfigurer value to the configurers. The assignment Order is based on the @order notation. We can see the Spring gives us wrote WebMvcAutoConfigurationAdapter @ Order (0). So if you don’t want to use it

converters.set(0, converter);
Copy the code

This writing but directly add, you can in custom WebMvcConfigurer will priority earlier than WebMvcAutoConfigurationAdapter can too.

What does Write do

Since the above has explained why is FastJsonHttpMessageConverter, then we can see its write method.

@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

    ByteArrayOutputStream outnew = new ByteArrayOutputStream();
    try {
        
        / /... omit
        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());
                
       / /... omit
    } catch (JSONException ex) {
        throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
    } finally{ outnew.close(); }}Copy the code

Skip some of the middlemen, and it ends up doing the core json-ification. The next step is to see if Fastjson reserves a hook like the one above for us during this process. Promise is affirmative, after all also is such excellent open source project. Let’s just go to the code

public class JavaBeanSerializer extends SerializeFilterable implements ObjectSerializer {

    protected void write(JSONSerializer serializer, //
                          Object object, //
                          Object fieldName, //
                          Type fieldType, //
                          int features,
                          boolean unwrapped
        ) throws IOException {
    
    	// This is how each key-value is processed
    	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); }}}}Copy the code

ValueFilters are executed every time Fastjson processes a key-value. So we only need to configure FastJsonHttpMessageConverter, take our custom ValueFilter, so that it will be processed. Let’s look at this interface

public interface ValueFilter extends SerializeFilter {

    Object process(Object object, String name, Object value);
}
Copy the code

We can see that there is a process method in this interface, so we can also use this hook to clean up the content before output JSON data.

So how to choose

Together, we can see that there are two hooks mentioned in the process

  1. SpringMVC writes the previous hook
  2. The Fastjson hook used by SpringMVC before writing JSON

Which hook do you think you should use? Personally, I would say Fastjson hooks. Because if you use the SpringMVC hooks, when you get the object, it’s likely that reflection will be used to modify the content, and subsequent serialization, whether Fastjson or Jackson, will also use reflection. Both rounds of reflection affect performance to some extent, so instead of fixing them both, leave them in the Fastjson hooks.

JSON data request

Having said that, let’s take a look at the JSON data request and see if there’s a solution. Speaking of requests, let’s return to our DispatcherServlet above.

DispatcherServlet

Going back to the class, here we start with doService and then call the doDispatch method. (What, you don’t even know why you’re in doService, I… Go out and turn left to find Tomcat source code.

/**
 * 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 {
	
	// Some configuration operations are ignored here
	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); }}}}Copy the code

In the doDispatch method, will be to find corresponding HandlerAdapter, namely RequestMappingHandlerAdapter this adapter.That’s the second little box in the picture above. As for the first one, which is the interview question you often memorize, ask SpringMVC request processing flow, where you get the interface handler. Of course, just to mention it.

So is how to decide the RequestMappingHandlerAdapter, we see the getHandlerAdapter methodAs you can see, there are four built-in handlerAdapters provided in the Spring container. RequestMappingHandlerAdapter didn’t rewrite the method supports, so use the abstract methods of AbstractHandlerMethodAdapter supports method

	/**
	 * 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));
	}
Copy the code

RequestMappingHandlerAdapter rewrite the supportsInternal method, identical in 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;
}
Copy the code

You can also look at the above English notes, or relatively easy to understand. The former, on the other hand, uses HandlerMethod as its handler class when encapsulating Http requests. So it is combination of these factors is decided RequestMappingHandlerAdapter as adapter.

InvocableHandlerMethod

In a twist of wind, we come to this class. After class adapter RequestMappingHandlerAdapter as treatment is determined, and then get into a series of operations. We don’t care about the other operations here, because we care about the parameters.The getMethodArgumentValues method here is used to get parameters from the interfaceThis. Resolvers calls the resolveArgument method

HandlerMethodArgumentResolverComposite

This. Resolvers above is the same classHere again the need for getArgumentResolver to obtain the parameters of the real processor HandlerMethodArgumentResolverI’m not going to say much about caches here. The system provides 26 parameter processors.

RequestResponseBodyMethodProcessor

In normal development, if the front-end page is passing json data, we add @requestBody in front of the SpringMVC parameter object. In the old days, even when we were doing it, all we had to do was add this annotation, and we could receive JSON data. So today we finally know that because of the annotation specification, the process locates the processor according to the supportsParameter interface method. (Underline, underline, underline)Entering the resolveArgument, we see something familiar again. Yes, here readWithMessageConverters and with MessageConverters we said before. Go into this method and look at its implementation

@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

	// The previous logic is omitted

	EmptyBodyCheckingHttpInputMessage message;
	try {
		message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

		for(HttpMessageConverter<? > converter :this.messageConverters) { Class<HttpMessageConverter<? >> converterType = (Class<HttpMessageConverter<? >>) converter.getClass(); GenericHttpMessageConverter<? > genericConverter = (converterinstanceofGenericHttpMessageConverter ? (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);
	}

	// omit the post-logic

	return body;
}
Copy the code

As you can see, this is the same writing method as before, but instead of returning JSON data, there is one more post processing. There are also two hooks:

  1. SpringMVC provides pre-processing or post-processing hooks for processing data
  2. HttpMessageConverter provides hook handling

FastJsonHttpMessageConverter

So let’s go back to this class. So this is writing JSON, this is reading JSON from IOThis brings us back to our usual json.parseObject () method. There’s a lot of configuration here, so are there hooks that we might use?

The answer is obvious. As the program executes, we come to the core of Fastjson parsingThis part is parsed from the IO stream into a string, followed by a call to parseObjectThere are three types of processers, which I can interpret as three filters. Unfortunately, these three filters are only called when no matching attribute is found.

It seems that finding hooks from Fastjson isn’t going to work (of course, there could be other hooks that I don’t know about, but they might be a little weak from what I’m seeing right now).

getAdvice()

Since the Fastjson approach may not work, let’s take a look at the hooks provided by 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);
Copy the code

GetAdvice here is () to obtain the type of RequestResponseBodyAdviceChain, it was among the AbstractMessageConverterMethodArgumentResolver properties, So let’s just pay attention to how it gets assigned.

From the source, the assignment is done inside the constructor, so where is the parameter passed inAccording to the DEBUG stack, can track to this, then this. Here how requestResponseBodyAdvice assignment? System will automatically loading JsonViewRequestBodyAdvice and JsonViewResponseBodyAdvice to us, I wouldn’t have opened here, we will see whether we can assign a custom hooks. Moving up the stack, we can take a look at the afterPropertiesSet.DEBUG is set at line 561, where the Advice provided by the system is loaded. I don’t know if you notice this method at this point

// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
Copy the code

Especially with this comment above, it seems that this place has what we wantThis method, the first is to find the ControllerAdviceBean find basis or according to the 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;
}
Copy the code

This class is located primarily by the @ControllerAdvice annotation and encapsulated as a ControllerAdviceBean object. If we go back to the figure above, is to implement the RequestBodyAdvice or ResponseBodyAdvice interface classes, will join the requestResponseBodyAdviceBeans among this list, And copies the Advice to our above said this. RequestResponseBodyAdvice, and is in the first place! As you can see, if we implement custom Advice, Spring wants to execute our Advice first. In addition, we can see that there is also ResponseBodyAdvice. Yes, this is where the hook is assigned to return the JSON data that was not selected above. To that end, we can simply implement this Advice

@RestControllerAdvice
public class XssRequestControllerAdvice implements RequestBodyAdvice {

    /** * Default XSS filter handles whitelist */
    public final static Whitelist DEFAULT_WHITE_LIST = Whitelist.relaxed().addAttributes(":all"."style");


    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class
       > converterType) {
        return methodParameter.hasParameterAnnotation(RequestBody.class);
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
       > converterType) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
       > converterType) throws IOException {
        return new HttpInputMessage() {
            @Override
            public InputStream getBody(a) 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(a) {
                returninputMessage.getHeaders(); }}; }@Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
       > converterType) {
        returnbody; }}Copy the code

Here I overwrite beforeBodyRead. Read strings from IO, after XSS data cleaning, and output as IO streams. However, the IO stream is read again in the read section.

summary

Through the above series of source code exploration (this wave of exploration really TM), mainly for JSON data XSS filtering scheme exploration and ideas. Finally, I made the following plan for this requirement:

  1. XssFilter is provided for XSS data filtering in response to form submission requests
  2. Provide XssRequestControllerAdvice and FastJsonXssValueFilter respectively for the request and the data in corresponding XSS filtering, this is in response to the request of the JSON data, the former can prevent XSS later writing data, The latter filters out dirty XSS data that already exists on the system, and users can correct XSS data by saving it.

The last

Color {red}{thumbnail}{thumbnail}{thumbnail}{thumbnail}{thumbnail}{thumbnail}{thumbnail}{thumbnail}{thumbnail} Also attached here is my Github address :github.com/showyool/ju…