Невозможно загрузить файл в Spring MVC с помощью Spring Security, несмотря на полную настройку - PullRequest
1 голос
/ 20 апреля 2020

Я пытаюсь загрузить файл .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 с неверным оператором токена

1 Ответ

0 голосов
/ 21 апреля 2020

После двух дней борьбы:

Конструктор должен содержать классы конфигурации контекста безопасности и приложения

public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {

    public SecurityInitializer() {
        super(SecurityContext.class, WebAppContext.class);
    }

}

Контекст приложения (WebAppContext) должен содержать MultipartResolver определение компонента

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

В моем случае после инициализации приложения токен csrf внутри Spring CsrfTokenRepository почему-то был пустым, поэтому, когда Spring сравнивал токен из заголовка клиентского запроса с null в CsrfFilter Spring возвращал ошибку 403 Я настроил csrf в контексте безопасности следующим образом:

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
    http.csrf().csrfTokenRepository(new CookieCsrfTokenRepository());
    ...    
}

Теперь токен csrf передается в файлах cookie с первым ответом сервера браузеру, а хранилище создает и кэширует токен для сравнения с тот, который приходит от клиента, поэтому сравнение проходит успешно

Здесь CookieCsrfTokenRepository также может быть объявлено как CookieCsrfTokenRepository.withHttpOnlyFalse(), если вы хотите получить токен из cook ie и установить его в заголовок csrf, но я выбрал go с мета-тегами, подходящими выше

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...