Другое

Jetty CORS Preflight: Руководство по Access-Control-Allow-Private-Network

Узнайте, как обрабатывать CORS preflight запросы с заголовком Access-Control-Allow-Private-Network в Jetty 11.0.17. Полное руководство с реализацией и тестированием пользовательского фильтра.

Как обрабатывать CORS preflight запросы с заголовком ‘Access-Control-Allow-Private-Network’ в Jetty 11.0.17?

Я использую сервер Jetty 11.0.17 и encountering следующую ошибку, когда удаленная страница пытается получить доступ к локальному Jetty Web серверу на 127.0.0.1:8080:

Доступ к XMLHttpRequest по адресу 'http://127.0.0.1:8080/api' из источника 'http://<some-url>' был заблокирован политикой CORS: Ответ на preflight запрос не прошел проверку контроля доступа: В ответе на preflight запрос для этого запроса к частной сети, нацеленного на локальное адресное пространство, отсутствовал заголовок 'Access-Control-Allow-Private-Network'.

Похоже, что заголовок ‘Access-Control-Allow-Private-Network’ пока не поддерживается в CrossOriginFilter Jetty (см. GitHub issue: https://github.com/jetty/jetty.project/issues/7642).

Я создал пользовательский фильтр для решения этой проблемы:

java
FilterHolder corsFilter = new FilterHolder(new Filter() {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        response.setHeader("Access-Control-Allow-Origin", "https://ccs-dev.cipher.kiev.ua");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
        response.setHeader("Access-Control-Allow-Private-Network", "true");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
            return;
        }

        chain.doFilter(req, res);
    }

    @Override public void init(FilterConfig filterConfig) {}
    @Override public void destroy() {}
});

contextHandler.addFilter(corsFilter, "/*", null);

Однако этот фильтр не обрабатывает preflight запросы должным образом. Как можно изменить этот пользовательский фильтр, чтобы правильно обрабатывать preflight запросы, включая заголовок ‘Access-Control-Allow-Private-Network’?

Обработка CORS preflight-запросов с заголовком ‘Access-Control-Allow-Private-Network’ в Jetty 11.0.17

Для обработки CORS preflight-запросов с заголовком ‘Access-Control-Allow-Private-Network’ в Jetty 11.0.17 необходимо реализовать пользовательский фильтр, который корректно обрабатывает preflight-запросы, включая необходимый заголовок. Jetty CrossOriginFilter в настоящее время не поддерживает заголовок ‘Access-Control-Allow-Private-Network’, что требует создания пользовательского решения.

Содержание

Понимание требований CORS Preflight

Ошибка, с которой вы сталкиваетесь, возникает потому, что Chrome теперь требует заголовок ‘Access-Control-Allow-Private-Network’ для запросов к частным сетевым адресам, таким как 127.0.0.1. Как указано в документации Chrome Private Network Access, “Preflight-запросы для PNA отправляются для всех запросов к частным сетям, независимо от метода запроса и режима”.

Когда удаленная страница пытается получить доступ к вашему локальному серверу Jetty, Chrome отправляет preflight-запрос OPTIONS для проверки, разрешает ли сервер кросс-оригинальные запросы. Если в ответ на этот preflight-запрос отсутствует заголовок ‘Access-Control-Allow-Private-Network: true’, браузер блокирует фактический запрос.

Правильная реализация пользовательского CORS-фильтра

Ваш текущий фильтр не корректно обрабатывает preflight-запросы, поскольку он только устанавливает заголовки, но не должным образом отвечает на запросы OPTIONS. Вот улучшенная реализация:

java
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;

public class CustomCORSFilter implements Filter {
    
    private String allowedOrigins;
    private String allowedMethods;
    private String allowedHeaders;
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Чтение конфигурации из web.xml или использование значений по умолчанию
        allowedOrigins = filterConfig.getInitParameter("allowedOrigins");
        if (allowedOrigins == null) {
            allowedOrigins = "*";
        }
        
        allowedMethods = filterConfig.getInitParameter("allowedMethods");
        if (allowedMethods == null) {
            allowedMethods = "GET, POST, OPTIONS, PUT, DELETE, HEAD";
        }
        
        allowedHeaders = filterConfig.getInitParameter("allowedHeaders");
        if (allowedHeaders == null) {
            allowedHeaders = "Content-Type, Authorization, X-Requested-With";
        }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // Установка CORS-заголовков для всех ответов
        response.setHeader("Access-Control-Allow-Origin", allowedOrigins);
        response.setHeader("Access-Control-Allow-Methods", allowedMethods);
        response.setHeader("Access-Control-Allow-Headers", allowedHeaders);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Private-Network", "true");
        
        // Обработка preflight-запросов
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setHeader("Access-Control-Max-Age", "3600"); // Кэширование preflight на 1 час
            response.setStatus(HttpServletResponse.SC_OK);
            return; // Прерывание цепочки для запросов OPTIONS
        }

        // Для фактических запросов продолжаем цепочку
        chain.doFilter(req, res);
    }

    @Override
    public void destroy() {
        // Очистка при необходимости
    }
}

Чтобы настроить этот фильтр в вашем сервере Jetty:

java
// Создание сервера и обработчика контекста
Server server = new Server(8080);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
server.setHandler(context);

// Добавление пользовательского CORS-фильтра
FilterHolder corsFilter = new FilterHolder(new CustomCORSFilter());
corsFilter.setInitParameter("allowedOrigins", "https://ccs-dev.cipher.kiev.ua");
corsFilter.setInitParameter("allowedMethods", "GET, POST, OPTIONS, PUT, DELETE");
corsFilter.setInitParameter("allowedHeaders", "Content-Type, Authorization, X-Requested-With");

context.addFilter(corsFilter, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));

// Добавление ваших сервлетов и запуск сервера
// ...
server.start();
server.join();

Альтернативные решения

1. Расширение CrossOriginFilter Jetty

Поскольку Jetty 11 содержит CrossOriginFilter, вы можете расширить его и добавить недостающий заголовок:

java
import org.eclipse.jetty.servlets.CrossOriginFilter;

public class ExtendedCrossOriginFilter extends CrossOriginFilter {
    
    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, 
                          FilterChain chain) throws IOException, ServletException {
        
        super.doFilter(request, response, chain);
        
        // Добавление заголовка частной сети после работы родительского фильтра
        if (request.getHeader("Origin") != null) {
            response.setHeader("Access-Control-Allow-Private-Network", "true");
        }
    }
}

2. Использование Jetty 12 или более поздних версий

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

3. Обходной путь с помощью командной строки Chrome

Для целей разработки вы можете запускать Chrome с отключенной безопасностью CORS:

bash
google-chrome --disable-web-security --user-data-dir=/tmp/chrome-dev

Предупреждение: Это только для разработки и никогда не должно использоваться в продакшене.

Тестирование и валидация

Чтобы проверить правильность конфигурации CORS:

  1. Используйте инструменты разработчика браузера для проверки preflight-запроса и ответа
  2. Убедитесь, что ответ на preflight содержит:
    • Access-Control-Allow-Origin: https://ccs-dev.cipher.kiev.ua
    • Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
    • Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
    • Access-Control-Allow-Private-Network: true
  3. Протестируйте фактический запрос, чтобы убедиться в его успешном выполнении

Полный пример конфигурации

Вот полная конфигурация встроенного Jetty с пользовательским CORS-фильтром:

java
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import javax.servlet.DispatcherType;
import java.util.EnumSet;

public class JettyServerWithCORS {
    
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        
        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/");
        server.setHandler(context);
        
        // Добавление пользовательского CORS-фильтра
        FilterHolder corsFilter = new FilterHolder(new CustomCORSFilter());
        corsFilter.setInitParameter("allowedOrigins", "https://ccs-dev.cipher.kiev.ua");
        corsFilter.setInitParameter("allowedMethods", "GET, POST, OPTIONS, PUT, DELETE");
        corsFilter.setInitParameter("allowedHeaders", "Content-Type, Authorization, X-Requested-With");
        
        context.addFilter(corsFilter, "/*", 
            EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
        
        // Добавление вашего REST-сервлета
        context.addServlet(new ServletHolder(new YourRestServlet()), "/api/*");
        
        // Запуск сервера
        server.start();
        server.join();
    }
}

Это решение корректно обрабатывает как preflight-запросы OPTIONS, так и фактические запросы, включая необходимый заголовок ‘Access-Control-Allow-Private-Network’. Фильтр настроен на кэширование ответов preflight для лучшей производительности и поддерживает различные HTTP-методы и заголовки.

Источники

  1. Local Network Access in Jetty - Stack Overflow
  2. Private Network Access: introducing preflights | Chrome for Developers
  3. CORS for private networks (RFC1918) warning on call to local service - Stack Overflow
  4. The Access-Control-Allow-Private-Network CORS header recently introduced in Google Chrome · Issue #7642 · jetty/jetty.project
  5. CrossOriginFilter (Jetty :: Project 12.0.29 API)

Заключение

Чтобы корректно обрабатывать CORS preflight-запросы с заголовком ‘Access-Control-Allow-Private-Network’ в Jetty 11.0.17:

  1. Реализуйте пользовательский CORS-фильтр, который обрабатывает как OPTIONS preflight-запросы, так и фактические запросы
  2. Включите необходимые заголовки: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, и важнейший Access-Control-Allow-Private-Network: true
  3. Корректно отвечайте на запросы OPTIONS с соответствующими кодами состояния и заголовками кэширования
  4. Правильно настройте фильтр в вашем сервере Jetty с правильными типами диспетчера
  5. Тщательно тестируйте, чтобы убедиться в корректной работе как preflight, так и фактических запросов

Подход с пользовательским фильтром обеспечивает гибкость для обхода текущих ограничений Jetty при соблюдении требований Chrome к доступу к частным сетям. Для производственных сред рекомендуется отслеживать проблемы на GitHub Jetty на предмет нативной поддержки заголовка Access-Control-Allow-Private-Network в будущих релизах.

Авторы
Проверено модерацией
Модерация