Как изменить заголовки ответа после успешного формирования ответа?

Всем привет! Многим знаком класс javax.servlet.Filter, который позволяет встраивать в жизненный цикл запроса свою логику и у меня с ним проблема:

public class CorsFilter implements Filter {

    private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
    private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
    private static final String ALLOW = "Allow";

    @Override
    public void init(FilterConfig filterConfig) {
        // NO LOGIC
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(req, resp);

        final HttpServletResponse response = (HttpServletResponse) resp;
        
        final Collection<String> headers = response.getHeaderNames();
        if (notContainHeaderWithIgnoreCase(headers, ACCESS_CONTROL_ALLOW_ORIGIN)) {
            response.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        }
        if (notContainHeaderWithIgnoreCase(headers, ACCESS_CONTROL_ALLOW_HEADERS)) {
            response.addHeader(ACCESS_CONTROL_ALLOW_HEADERS, "*");
        }
        if (notContainHeaderWithIgnoreCase(headers, ACCESS_CONTROL_ALLOW_METHODS)) {
            response.addHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
        }
        if (notContainHeaderWithIgnoreCase(headers, ALLOW)) {
            response.addHeader(ALLOW, "GET, POST, PUT, DELETE, OPTIONS");
        }
    }

    private boolean notContainHeaderWithIgnoreCase(Collection<String> headers, String name) {
        return headers.stream().noneMatch(header -> header.equalsIgnoreCase(name));
    }

    @Override
    public void destroy() {
        // NO LOGIC
    }
}

В данном фильтре я пытаюсь после формирования ответа посмотреть: есть ли в нём CORS заголовки и, если нет, то проставить их. Сделать это ДО формирования ответа я не могу, так как сервис иногда проксирует запрос и сторонние сервисы тоже проставляют CORS тем самым получается дублирование.

Такая реализация не работает, так как после формирования ответа он помечается как commited и изменять его нельзя. Как быть?


Ответы (1 шт):

Автор решения: ulxanxv

С проблемой разобрался, получилось вот так (может быть кому то будет нужно):

public class CorsFilter implements Filter {

    private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
    private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
    private static final String ALLOW = "Allow";

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException {
        final ResponseWrapper wrapper = new ResponseWrapper(
                (HttpServletResponse) resp);

        filterChain.doFilter(req, wrapper);

        setCorsHeaders((HttpServletResponse) resp);
        copyResponseToOriginalResponse(wrapper, resp);
    }

    private void setCorsHeaders(HttpServletResponse response) {
        final HeaderNames headerNames = new HeaderNames(response.getHeaderNames());
        if (headerNames.notContainsIgnoreCase(ACCESS_CONTROL_ALLOW_ORIGIN)) {
            response.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        }
        if (headerNames.notContainsIgnoreCase(ACCESS_CONTROL_ALLOW_HEADERS)) {
            response.addHeader(ACCESS_CONTROL_ALLOW_HEADERS, "*");
        }
        if (headerNames.notContainsIgnoreCase(ACCESS_CONTROL_ALLOW_METHODS)) {
            response.addHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
        }
        if (headerNames.notContainsIgnoreCase(ALLOW)) {
            response.addHeader(ALLOW, "GET, POST, PUT, DELETE, OPTIONS");
        }
    }

    private void copyResponseToOriginalResponse(ResponseWrapper wrapper, ServletResponse response) throws IOException {
        final ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(wrapper.getCaptureAsBytes());
    }

    private static final class ResponseWrapper extends HttpServletResponseWrapper {

        private final ByteArrayOutputStream capture;
        private ServletOutputStream output;
        private PrintWriter writer;

        public ResponseWrapper(HttpServletResponse response) {
            super(response);
            capture = new ByteArrayOutputStream(response.getBufferSize());
        }

        @Override
        public ServletOutputStream getOutputStream() {
            if (writer != null) {
                throw new IllegalStateException("getWriter() has already been called on this response.");
            }

            if (output == null) {
                output = new ServletOutputStream() {
                    @Override
                    public void write(int b) throws IOException {
                        capture.write(b);
                    }

                    @Override
                    public void flush() throws IOException {
                        capture.flush();
                    }

                    @Override
                    public void close() throws IOException {
                        capture.close();
                    }

                    @Override
                    public boolean isReady() {
                        return false;
                    }

                    @Override
                    public void setWriteListener(WriteListener arg0) {
                    }
                };
            }

            return output;
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            if (output != null) {
                throw new IllegalStateException("getOutputStream() has already been called on this response.");
            }

            if (writer == null) {
                writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()));
            }

            return writer;
        }

        @Override
        public void flushBuffer() throws IOException {
            super.flushBuffer();

            if (writer != null) {
                writer.flush();
            } else if (output != null) {
                output.flush();
            }
        }

        public byte[] getCaptureAsBytes() throws IOException {
            if (writer != null) {
                writer.close();
            } else if (output != null) {
                output.close();
            }

            return capture.toByteArray();
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
        /* NO LOGIC */
    }

    @Override
    public void destroy() {
        /* NO LOGIC */
    }
}
→ Ссылка