[Хардкор] Исследуйте SpringMVC и FastJson из проблемы XSS | Бычье новогоднее эссе

Java

предисловие

Прошло много времени с тех пор, как я писал статью, поэтому по чьему-то настоянию я резюмирую недавнее требование. В процессе разработки этого требования еще много наработок, поэтому поделюсь, если есть какие-то дополнения и неточности, прошу критиковать и исправлять.

задний план

Давайте сначала поговорим о предыстории этого требования. Увидев этот заголовок, вы можете подумать о том, нужно ли фильтровать систему, потому что система подверглась XSS-атаке. Правильно, это потому, что несколько дней назад глупая партия отправила часть контента, содержащую скрипт, и результат был успешно сохранен, а затем, когда он был показан, браузер прямо уведомил эту подсказку (распространенные лазейки в традиционных проектах, простите меня) , что также является относительно распространенным сценарием атаки XSS. Для этого необходимо отфильтровать содержимое XSS, и выполнить совместимую обработку грязных данных в системе (позже, после тщательного поиска в системе действительно много XSS-контента, который оценивается как дружеская работа друзей).

анализ спроса

Увидев это, по оценкам, многие маленькие партнеры усмехнулись, думая в глубине души: «Эта вещь действительно слишком педиатрическая для меня». Итак, Baidu щелкнул и шлепнул меня кодом по лицу. Эти ответы примерно такие: Предоставить фильтр Xss, затем инкапсулировать Запрос в этот фильтр, и переписать внутри метод 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-контента, для конкретного использования вы можете самостоятельно использовать Baidu.

Я думаю, что вышеизложенное должно быть более распространенным способом написания. Конечно, в этом способе записи нет ничего плохого, но на самом деле есть ограничение, то есть этот способ подходит только для запроса, отправленного формой формы, и если это вид запроса данных JSON, данные не будет получен, и большинство запросов нашего системного интерфейса отправляются с данными JSON.
Кроме того, есть еще одна проблема, то есть в нашей системе уже есть грязные данные XSS, поэтому для решения этой проблемы есть две перспективы:

  1. Работа с грязными данными в системе, то есть очистка SQL.

2. Очистка данных выполняется при возврате данных.

Для первого решения, поскольку система представляет собой подбазу данных и подтаблицу, а бизнес-таблицы в этот период избыточны, очистить SQL очень сложно, и возникают огромные риски и скрытые опасности. Поэтому мы по-прежнему используем здесь второе решение.Поскольку данные возвращаются в формате JSON, нам нужно только выполнить общее преобразование для очистки данных.
Итак, подытожим данные о том, что необходимо сделать:

  1. Запрос данных очистки данных для JSON
  2. Очистка данных для возврата данных 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 в messageConverts внутри Spring, и эффект тот же.

DispatcherServlet

Для этого класса, я думаю, мы должны быть знакомы с ним, даже если мы не видели исходный код. Когда вы только изучаете SpringMVC, различные учебные пособия сообщают вам, что вам нужно настроить этот класс в файле конфигурации web.xml. Это связано с тем, что класс DispatcherServlet является классом входа для всех запросов SpringMVC. Когда Tomcat упаковывает запрос и передает его HttpServlet, HttpServlet в это время на самом деле является FrameworkServlet, предоставляемым Spring, а метод doService, который вводится следующим, реализуется FrameworkServlet, то есть DispatcherServlet. как показано на рисунке:Тогда в этом методе doDispatch после вызова послойно будет вызываться HandlerMethodReturnValueHandlerComposite#handleReturnValue (среднюю ссылку пропущу, отлаживать можно и самостоятельно)

	@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. Через этот интерфейс мы определяем, какой процессор используется во время выполнения. Здесь мы рассмотрим метод supportsReturnType, реализованный RequestResponseBodyMethodProcessor:

	@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;
        }
    }
}

Может показаться, что этот код громоздкий, но на самом деле есть два ключевых шага:

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

Правильно, метод записи заключается в том, что мы возвращаем данные JSON в поток ввода-вывода, а getAdvice().beforeBodyWrite() — это предварительный метод, который может обрабатывать данные перед записью, а также это ловушка, которую Spring оставляет разработчикам. Так что это может быть место для рассмотрения, которое будет упомянуто ниже, и мы продолжим смотреть вниз.

FastJsonHttpMessageConverter

Вернемся к циклу for выше и посмотрим на this.messageConverters.

Почему FastJsonHttpMessageConverter

Мы видим, что SpringMVC имеет множество встроенных HttpMessageConverters. This.messageConverters будет загружен при старте контейнера Spring, встроенная система будет загружена первой, а затем последней будет загружена наша кастомная, то есть наш главный герой FastJsonHttpMessageConverter. Они знают, почему я хочу написать

converters.set(0, converter);

Цель состоит в том, чтобы сначала использовать FastJsonHttpMessageConverter при обходе, в противном случае MappingJackson2HttpMessageConverter выполняется по умолчанию. (Надеюсь, когда-нибудь FastJsonHttpMessageConverter также станет встроенным конвертером)

Давайте поговорим о том, как загружается this.messageConverters.

Ну этот процесс я взял одним махом выше, хотел полениться, а обнаружил ваше любопытство. Как упоминалось выше, this.messageConverters будет загружен при запуске контейнера Spring, встроенная система будет загружена первой, а наша кастомная — последней. Так как он загружается?
Давайте сначала взглянем на DUBUG и будем двигаться дальше по результатам. Проверьте стек здесьМожно обнаружить, что в списке входящих преобразователей уже 10 встроенных преобразователей, так что здесь это означает, что сначала назначаются встроенные преобразователи системы, а затем уже наша очередь их настраивать.
Что ж, я думаю, вы явно недовольны этим объяснением, так что давайте продолжим и посмотрим, куда здесь входит программа:Я хочу видеть это, и я всегда должен понимать, что в списке this.delegates есть два класса реализации WebMvcConfigurer, которые проходятся по порядку. Вы можете видеть, что наш пользовательский WebMvcConfigurer более поздний, чем встроенный в Spring WebMvcAutoConfigurationAdapter.
Э-э, нет, вы даже спросите, почему порядок в этом делегатах такой?
Тогда давайте посмотрим, как вышеперечисленное назначается этому делегату.Вы можете видеть, что этот WebMvcConfigurer также передается извне, давайте посмотрим, где он передаетсяЯ думаю, что такой способ написания должен быть очень ясным. Согласно Autowired, WebMvcConfigurer в контейнере назначается конфигураторам. Что касается порядка в процессе назначения, он будет относиться к тому, есть ли аннотация @Order. Мы можем видеть WebMvcAutoConfigurationAdapter, предоставленный Spring.@Order(0) написан выше. Так что, если вы, боссы, не хотите использовать

converters.set(0, converter);

Этот способ написания напрямую добавляется, тогда также можно установить приоритет перед WebMvcAutoConfigurationAdapter в пользовательском WebMvcConfigurer.

что написал делать

Поскольку выше было объяснено, почему это FastJsonHttpMessageConverter, мы можем посмотреть на его метод записи.

@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 обрабатывает каждое значение ключа, он будет выполнять фильтры этого значения. Поэтому нам нужно только добавить наш собственный ValueFilter при настройке FastJsonHttpMessageConverter, чтобы он был обработан. Давайте посмотрим на этот интерфейс

public interface ValueFilter extends SerializeFilter {

    Object process(Object object, String name, Object value);
}

Мы видим, что в этом интерфейсе есть метод процесса, поэтому мы также можем использовать этот хук для очистки и изменения содержимого перед выводом данных JSON.

Итак, как выбрать

Итак, в целом, мы можем обнаружить, что во всем процессе будут упомянуты два хука.

  1. SpringMVC записывает хук до
  2. Перехватчики Fastjson, используемые SpringMVC до написания 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 встроенных обработчика. RequestMappingHandlerAdapter не переопределяет этот метод поддержки, поэтому он использует метод поддержки в абстрактном методе AbstractHandlerMethodAdapter.

	/**
	 * 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;
}

Вы также можете посмотреть английские комментарии выше, которые относительно легко понять.
В первом случае HandlerMethod используется как класс обработки в инкапсулированном Http-запросе. Поэтому путем объединения этих факторов определяется, что в качестве адаптера используется RequestMappingHandlerAdapter.

InvocableHandlerMethod

Как только изменился стиль рисования, мы пришли в этот класс. После того как RequestMappingHandlerAdapter будет определен как адаптер класса обработки, последует ряд операций. Здесь нас не интересуют другие операции, потому что нас интересует работа части параметров.Метод getMethodArgumentValues ​​здесь используется для получения параметров в интерфейсеВ частности, this.resolvers вызывает метод resolveArgument для обработки

HandlerMethodArgumentResolverComposite

Вышеупомянутый this.resolvers является этим классомЗдесь снова необходимо выполнить getArgumentResolver, чтобы получить обработчик реальных параметров HandlerMethodArgumentResolver.Я не буду много говорить о кеше здесь. В системе предусмотрено 26 обработчиков параметров.

RequestResponseBodyMethodProcessor

В нашей обычной разработке, если интерфейсная страница передает данные json, мы добавим @RequestBody перед объектом параметра SpringMVC. Когда я учился, в том числе когда я этим занимался, во всяком случае, пока эта аннотация была добавлена, данные в формате 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.
Тогда мы также можем предположить, что здесь есть два хука:

  1. Хуки предварительной или последующей обработки, предоставляемые данными процесса SpringMVC.
  2. Обработка ловушек, предоставляемая HttpMessageConverter

FastJsonHttpMessageConverter

Вернемся снова к этому классу. Выше написано JSON, здесь нужно читать JSON из IOЗатем мы возвращаемся к обычно используемому нами методу JSON.parseObject(). Здесь представлено множество конфигураций, так есть ли какие-нибудь хуки, которые мы могли бы здесь использовать?
Ответ очевиден. После выполнения программы мы подошли к основной части парсинга Fastjson.Эта часть анализируется из потока ввода-вывода в строку с последующим вызовом parseObject.Здесь предусмотрено три типа процессоров, которые я также могу понимать как три типа фильтров. Жаль, что эти три фильтра вызываются только тогда, когда не найдено подходящего свойства.
Похоже, что схема поиска крючков от 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, поэтому нам просто нужно обратить внимание на то, как он назначен (всегда кажется, что вы снова будете стравлены).
Из исходного кода присваивание выполняется в конструкторе, затем смотрите, куда передаются параметры.По ситуации со стеком DEBUG это можно отследить, так как же здесь назначается this.requestResponseBodyAdvice? Система автоматически загрузит для нас JsonViewRequestBodyAdvice и JsonViewResponseBodyAdvice. Я не буду здесь его раскрывать. Давайте посмотрим, сможем ли мы назначить наши пользовательские хуки. Затем поднимитесь по стеку, мы можем посмотреть на этот afterPropertiesSet.В настоящее время DEBUG установлен в строке 561, что является советом, предоставляемым системой загрузки. В настоящее время я не знаю, все ли заметили этот метод

// 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, и он будет в первую очередь! Видно, что если мы реализуем пользовательский совет, Spring хочет сначала выполнить наш совет. Кроме того, мы видим, что здесь также есть ResponseBodyAdvice, да, в этом месте также назначены хуки, участвующие в возврате данных JSON, которые не выбраны выше.
С этой целью мы можем просто реализовать этот Совет

@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. Чтение строк из ввода-вывода после очистки данных XSS, а затем вывод в потоки ввода-вывода. Хотя в части чтения будет выполняться чтение другого IO-потока.

резюме

Благодаря приведенной выше серии исследований исходного кода (эта волна исследований на самом деле является ТМ), в основном это исследование и идея схемы фильтрации XSS для данных JSON. В конце концов, я сделал следующее решение для этого требования:

  1. Предоставьте XssFilter для фильтрации данных XSS, это связано с методом запроса отправки формы.
  2. Предоставьте XssRequestControllerAdvice и FastJsonXssValueFilter для выполнения XSS-фильтрации запроса и соответствующих данных соответственно. Это связано с методом запроса данных JSON. Первый может предотвратить последующую запись данных XSS, а второй может фильтровать грязные данные XSS, которые уже существует в системе, и данные XSS могут быть исправлены пользователем, выполняющим операцию сохранения.

наконец

Если моя статья была вам полезна, я надеюсь, что вы, ребята,обращать внимание\color{red}{Нажмите, чтобы подписаться} поставить лайк\цвет{красный}{Нравится}, Еще раз спасибо за вашу поддержку!
Вот также мой адрес Github:GitHub.com/showyoOL/Согласно…