Я пытаюсь загрузить файл .pdf с jQuery AJAX в Spring MVC 5, когда серверная часть Spring Security 5 работает на Tomcat и столкнулась с множеством проблем в зависимости от конфигурации Spring
ПРИМЕЧАНИЕ:
Загрузка файла должна быть доступна без аутентификации
Внешний интерфейс
Разметка:
<div id="upload-modal" class="modal">
<div class="modal-content">
<h4>Upload</h4>
<form action="#" enctype="multipart/form-data">
<div class="file-field input-field">
<div class="btn">
<span>View...</span>
<input type="file" name="file" accept="application/pdf">
</div>
<div class="file-path-wrapper">
<label>
<input class="file-path validate" type="text">
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<a href="#" class="modal-close waves-effect waves-green btn-flat">Cancel</a>
<a href="#" id="upload-bttn" class="waves-effect waves-light btn-flat btn">Upload</a>
</div>
</div>
csrf
заголовок для всех запросов:
$(document).ready(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
Загрузка с jQuery AJAX:
$("#upload-bttn").click(function () {
var $uploadModal = $("#upload-modal");
const fileName = $uploadModal.find(".file-path").val();
const extension = fileName.substr(fileName.lastIndexOf(".") + 1);
if (extension === "pdf") {
$.ajax({
url: "/upload",
type: "POST",
data: new FormData($uploadModal.find("form").get(0)),
processData: false,
contentType: false,
success: function () {
console.log("success")
},
error: function () {
console.log("error")
}
});
} else {
M.toast({html: 'Selected file is not .pdf'});
}
});
Back-end
Общая конфигурация выглядит следующим образом. Он изменяется в зависимости от случаев
Инициализация безопасности:
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(SecurityContext.class);
}
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
Инициализация приложения:
public class ApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));
servletContext.getSessionCookieConfig().setHttpOnly(true);
servletContext.getSessionCookieConfig().setSecure(true);
AnnotationConfigWebApplicationContext dispatcherServlet = new AnnotationConfigWebApplicationContext();
dispatcherServlet.register(WebAppContext.class);
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(dispatcherServlet));
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
}
}
Дело 1 - Определение компонента CommonsMultipartResolver
CommonsMultipartResolver
определение компонента:
@Bean
public CommonsMultipartResolver multipartResolver(
@Value("${max.upload.size}") Integer maxNumber,
@Value("${max.size}") Integer maxSize) {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(1024 * maxSize * maxNumber);
resolver.setMaxUploadSizePerFile(maxSize);
resolver.setMaxInMemorySize(maxSize);
resolver.setDefaultEncoding("UTF-8");
try {
resolver.setUploadTempDir(new FileSystemResource(System.getProperty("java.io.tmpdir")));
} catch (IOException e) {
e.printStackTrace();
}
return resolver;
}
Я помню странное поведение Spring, когда компонент MultipartResolver
должен называться "multipartResolver" в явном виде. Я пробовал и @Bean
и @Bean("multipartResolver")
с конфигурацией выше и имел тот же результат (несмотря на то, что bean-компонент выше назван "multipartResolver" в соответствии с именем метода)
Результат:
Ошибка 500 - Невозможно обработать детали, так как не была предоставлена конфигурация из нескольких частей
Случай 2 - MultipartConfigElement в реестре сервлетов
- удалено
CommonsMultipartResolver
bean - добавлено
StandardServletMultipartResolver
боб - добавлено
MultipartConfigElement
к ApplicationInitializer
StandardServletMultipartResolver
определение бина:
@Bean
public StandardServletMultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
Обновлено ApplicationInitializer
:
@Override
public void onStartup(ServletContext servletContext) {
...
servlet.setMultipartConfig(new MultipartConfigElement(
System.getProperty("java.io.tmpdir")
));
}
Согласно документации Spring:
Убедитесь, что MultipartFilter указан перед фильтром Spring Security. Указание MultipartFilter после фильтра Spring Security означает, что нет авторизации для вызова MultipartFilter, что означает, что любой может разместить временные файлы на вашем сервере. Однако, только авторизованные пользователи смогут отправлять файл, который обрабатывается вашим приложением
Поскольку мне нужно разрешить пользователям, не прошедшим проверку, загружать файлы, которые я пробовал как до , так и после в SecurityInitializer
, как показано ниже, с тем же результатом
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
или
@Override
protected void afterSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
Результат:
Ошибка 403
Вопросы
- Что мне не хватает в конфигурации?
Мысли
CommonsMultipartResolver
будут предпочтительнее, так как позволяет управлять им со свойствами Spring - Что-то не так с настройкой контекста Spring Security
- Существует опция
allowCasualMultipartParsing="true"
(не тестировалась), которую я не хотел бы придерживаться в качестве ее Спецификация Tomcat c
Обновления
- При отключенном Spring Security все работает правильно
http.authorizeRequests().antMatchers("/**").permitAll();
остается единственной конфигурацией контекста безопасности, поэтому не Не думаю, что проблема с настройкой контекста безопасности - Установить компонент многочастного разрешения ame явно в
MultipartFilter
в beforeSpringSecurityFilterChain(ServletContext servletContext)
и все равно не повезло - Добавление токена
_csrf
в заголовок запроса не сработало в обоих случаях - Понял, что я пропускаю дополнительные
WebAppContext
класс в SecurityInitializer
конструкторе. Теперь ошибка 500 исчезла, но 403 появилось для случая 1. В журнале говорится, что у меня неверный токен csrf
, несмотря на то, что я добавил его в заголовок, как указано выше - Попытка отправить форму с токеном
csrf
, включая скрытый ввод <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
но результат тот же - ошибка 403 с неверным оператором токена