Сервлет для подачі статичного вмісту


145

Я розгортаю веб-сервер на двох різних контейнерах (Tomcat і Jetty), але сервлети за замовчуванням для подання статичного вмісту мають інший спосіб обробки URL-адреси, яку я хочу використовувати ( деталі ).

Тому я прагну включити невеликий сервлет у веб-сервер для обслуговування власного статичного вмісту (зображення, CSS тощо). Сервлет повинен мати такі властивості:

  • Ніяких зовнішніх залежностей
  • Простий і надійний
  • Підтримка If-Modified-Sinceзаголовка (тобто спеціальний getLastModifiedметод)
  • (Додатково) підтримка кодування gzip, етагів, ...

Чи є такий сервлет десь? Найближчий я можу знайти приклад 4-10 з книги servlet.

Оновлення: Структура URL-адреси, яку я хочу використовувати - якщо вам цікаво - це просто:

    <servlet-mapping>
            <servlet-name>main</servlet-name>
            <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
            <servlet-name>default</servlet-name>
            <url-pattern>/static/*</url-pattern>
    </servlet-mapping>

Тому всі запити повинні бути передані до основного сервлету, якщо вони не для staticшляху. Проблема полягає в тому, що серветт за замовчуванням Tomcat не враховує ServletPath (тому він шукає статичні файли в головній папці), тоді як Jetty (так це виглядає в staticпапці).


Чи можете ви детальніше розглянути "структуру URL-адрес", яку ви хочете використовувати? Прокрутка власного власного базування на прикладі зв'язаного прикладу 4-10 здається тривіальним зусиллям. Я робив це сам багато разів ...
Стю Томпсон,

Я редагував своє запитання, щоб розробити структуру URL-адрес. І так, я закінчив катати свою власну серветку. Дивіться мою відповідь нижче.
Бруно Де Фрейне

1
Чому ви не використовуєте веб-сервер для статичного вмісту?
Стівен

4
@Stephen: тому що не завжди Apache перед Tomcat / Jetty. І щоб уникнути клопоту окремої конфігурації. Але ви праві, я міг би розглянути такий варіант.
Бруно Де Фрейне

Я просто не можу зрозуміти, чому ви не використовували картографування, як це <servlet-mapping> <servlet-name> за замовчуванням </servlet-name> <url-pattern> / </url-pattern> </ servlet-mapping > для обслуговування статичного контенту
Maciek Kreft

Відповіді:


53

Я придумав дещо інше рішення. Це трохи хак-іш, але ось карти:

<servlet-mapping>   
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
 <servlet-name>default</servlet-name>
    <url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>myAppServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

В основному це просто відображає всі файли вмісту шляхом розширення на сервлет за замовчуванням, а все інше на "myAppServlet".

Він працює і в Jetty, і в Tomcat.


13
насправді ви можете додати більше ніж один тег узору URL-адреси всередині сервет-карти;)
Fareed Alnamrouti,

5
Servlet 2.5 і новіші підтримують кілька тегів узору URL-адреси всередині сервлет-карти
vivid_voidgroup

Будьте обережні з файлами індексу (index.html), оскільки вони можуть мати перевагу над вашим сервлетом.
Андрес

Я думаю, це погана ідея використання *.sth. Якщо хтось отримає URL, example.com/index.jsp?g=.sthвін отримає джерело файлу jsp. Або я помиляюся? (Я новачок у Java EE) Я зазвичай використовую шаблон URL-адреси /css/*тощо
SemperPeritus

46

У цьому випадку немає необхідності повністю користувацької реалізації сервлета за замовчуванням, ви можете використовувати цей простий сервлет, щоб обернути запит до реалізації контейнера:


package com.example;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

public class DefaultWrapperServlet extends HttpServlet
{   
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        RequestDispatcher rd = getServletContext().getNamedDispatcher("default");

        HttpServletRequest wrapped = new HttpServletRequestWrapper(req) {
            public String getServletPath() { return ""; }
        };

        rd.forward(wrapped, resp);
    }
}

Це питання має чіткий спосіб відображення / до контролера та / статичний до статичного вмісту за допомогою фільтра. Перевірте схвалену відповідь після прийнятої: stackoverflow.com/questions/870150/…
Девід Карбоні


30

Я мав хороші результати з FileServlet , оскільки він підтримує майже весь HTTP (етаги, ченкінг тощо).


Дякую! години невдалих спроб і поганих відповідей, і це вирішило мою проблему
Йоссі Шашо

4
Хоча для обслуговування вмісту з папки поза додатком (я використовую його для серверної папки з диска, скажімо, C: \ ресурси) я змінив цей рядок: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath ")); І замінив його на: this.basePath = getInitParameter ("basePath");
Йоссі Шашо

1
Оновлена ​​версія доступна на сайті showcase.omniface.org/servlets/FileServlet
koppor

26

Абстрактний шаблон для серветки статичного ресурсу

Частково на основі цього блогу з 2007 року, ось модернізований і багаторазовий абстрактний шаблон сервлета, який належним чином займається кешуванням ETag, If-None-Matchта If-Modified-Since(але не підтримує Gzip і Range; просто для простоти; Gzip може бути зроблено з фільтром або через конфігурація контейнера).

public abstract class StaticResourceServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);
    private static final String ETAG_HEADER = "W/\"%s-%s\"";
    private static final String CONTENT_DISPOSITION_HEADER = "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s";

    public static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.DAYS.toMillis(30);
    public static final int DEFAULT_STREAM_BUFFER_SIZE = 102400;

    @Override
    protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException ,IOException {
        doRequest(request, response, true);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doRequest(request, response, false);
    }

    private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException {
        response.reset();
        StaticResource resource;

        try {
            resource = getStaticResource(request);
        }
        catch (IllegalArgumentException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        if (resource == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String fileName = URLEncoder.encode(resource.getFileName(), StandardCharsets.UTF_8.name());
        boolean notModified = setCacheHeaders(request, response, fileName, resource.getLastModified());

        if (notModified) {
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        setContentHeaders(response, fileName, resource.getContentLength());

        if (head) {
            return;
        }

        writeContent(response, resource);
    }

    /**
     * Returns the static resource associated with the given HTTP servlet request. This returns <code>null</code> when
     * the resource does actually not exist. The servlet will then return a HTTP 404 error.
     * @param request The involved HTTP servlet request.
     * @return The static resource associated with the given HTTP servlet request.
     * @throws IllegalArgumentException When the request is mangled in such way that it's not recognizable as a valid
     * static resource request. The servlet will then return a HTTP 400 error.
     */
    protected abstract StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException;

    private boolean setCacheHeaders(HttpServletRequest request, HttpServletResponse response, String fileName, long lastModified) {
        String eTag = String.format(ETAG_HEADER, fileName, lastModified);
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME_IN_MILLIS);
        return notModified(request, eTag, lastModified);
    }

    private boolean notModified(HttpServletRequest request, String eTag, long lastModified) {
        String ifNoneMatch = request.getHeader("If-None-Match");

        if (ifNoneMatch != null) {
            String[] matches = ifNoneMatch.split("\\s*,\\s*");
            Arrays.sort(matches);
            return (Arrays.binarySearch(matches, eTag) > -1 || Arrays.binarySearch(matches, "*") > -1);
        }
        else {
            long ifModifiedSince = request.getDateHeader("If-Modified-Since");
            return (ifModifiedSince + ONE_SECOND_IN_MILLIS > lastModified); // That second is because the header is in seconds, not millis.
        }
    }

    private void setContentHeaders(HttpServletResponse response, String fileName, long contentLength) {
        response.setHeader("Content-Type", getServletContext().getMimeType(fileName));
        response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, fileName));

        if (contentLength != -1) {
            response.setHeader("Content-Length", String.valueOf(contentLength));
        }
    }

    private void writeContent(HttpServletResponse response, StaticResource resource) throws IOException {
        try (
            ReadableByteChannel inputChannel = Channels.newChannel(resource.getInputStream());
            WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream());
        ) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE);
            long size = 0;

            while (inputChannel.read(buffer) != -1) {
                buffer.flip();
                size += outputChannel.write(buffer);
                buffer.clear();
            }

            if (resource.getContentLength() == -1 && !response.isCommitted()) {
                response.setHeader("Content-Length", String.valueOf(size));
            }
        }
    }

}

Використовуйте його разом із наведеним нижче інтерфейсом, що представляє статичний ресурс.

interface StaticResource {

    /**
     * Returns the file name of the resource. This must be unique across all static resources. If any, the file
     * extension will be used to determine the content type being set. If the container doesn't recognize the
     * extension, then you can always register it as <code>&lt;mime-type&gt;</code> in <code>web.xml</code>.
     * @return The file name of the resource.
     */
    public String getFileName();

    /**
     * Returns the last modified timestamp of the resource in milliseconds.
     * @return The last modified timestamp of the resource in milliseconds.
     */
    public long getLastModified();

    /**
     * Returns the content length of the resource. This returns <code>-1</code> if the content length is unknown.
     * In that case, the container will automatically switch to chunked encoding if the response is already
     * committed after streaming. The file download progress may be unknown.
     * @return The content length of the resource.
     */
    public long getContentLength();

    /**
     * Returns the input stream with the content of the resource. This method will be called only once by the
     * servlet, and only when the resource actually needs to be streamed, so lazy loading is not necessary.
     * @return The input stream with the content of the resource.
     * @throws IOException When something fails at I/O level.
     */
    public InputStream getInputStream() throws IOException;

}

Все, що вам потрібно, це просто вийти з даного абстрактного сервлета і реалізувати getStaticResource()метод згідно з javadoc.

Конкретний приклад подачі з файлової системи:

Ось конкретний приклад, який подає його за URL-адресою, наприклад, /files/foo.extз локальної дискової файлової системи:

@WebServlet("/files/*")
public class FileSystemResourceServlet extends StaticResourceServlet {

    private File folder;

    @Override
    public void init() throws ServletException {
        folder = new File("/path/to/the/folder");
    }

    @Override
    protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException();
        }

        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        final File file = new File(folder, Paths.get(name).getFileName().toString());

        return !file.exists() ? null : new StaticResource() {
            @Override
            public long getLastModified() {
                return file.lastModified();
            }
            @Override
            public InputStream getInputStream() throws IOException {
                return new FileInputStream(file);
            }
            @Override
            public String getFileName() {
                return file.getName();
            }
            @Override
            public long getContentLength() {
                return file.length();
            }
        };
    }

}

Конкретний приклад подачі з бази даних:

Ось конкретний приклад, який подає його за такою URL-адресою, як /files/foo.extз бази даних, через виклик служби EJB, який повертає вашу організацію, яка має byte[] contentвластивість:

@WebServlet("/files/*")
public class YourEntityResourceServlet extends StaticResourceServlet {

    @EJB
    private YourEntityService yourEntityService;

    @Override
    protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException();
        }

        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        final YourEntity yourEntity = yourEntityService.getByName(name);

        return (yourEntity == null) ? null : new StaticResource() {
            @Override
            public long getLastModified() {
                return yourEntity.getLastModified();
            }
            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(yourEntityService.getContentById(yourEntity.getId()));
            }
            @Override
            public String getFileName() {
                return yourEntity.getName();
            }
            @Override
            public long getContentLength() {
                return yourEntity.getContentLength();
            }
        };
    }

}

1
Шановний @BalusC Я думаю , що ваш підхід вразливий для хакера, який поданий наступний запит може переміщатися корита файлової системи: files/%2e%2e/mysecretfile.txt. Цей запит виробляє files/../mysecretfile.txt. Я тестував його на Tomcat 7.0.55. Вони називають це каталог, що піднімається: owasp.org/index.php/Path_Traversal
Cristian Arteaga

1
@ Крістіан: Так, можливо. Я оновив приклад, щоб показати, як це запобігти.
BalusC

Це не повинно отримати відгуків. Обслуговування статичних файлів для веб-сторінки з сервлетом, як це, є рецептом розумної безпеки. Усі подібні проблеми вже вирішені, і немає підстав для впровадження нового способу користування, імовірно, більше віднайдених бомб часу безпеки не вдасться. Правильний шлях полягає в тому, щоб налаштувати Tomcat / GlassFish / Jetty і т.д. для обслуговування вмісту, а ще краще використовувати спеціалізований сервер файлів, наприклад NGinX.
Леонгард Принтз

@LeonhardPrintz: Я видалю відповідь і повідомлю своїх друзів у Tomcat, як тільки ви вкажете на проблеми безпеки. Нема проблем.
BalusC

19

Я закінчила катати свою StaticServlet. Він підтримує If-Modified-Sincegzip-кодування, і він повинен мати можливість обслуговувати статичні файли з файлів війни. Це не дуже складний код, але він теж не зовсім банальний.

Код доступний: StaticServlet.java . Сміливо коментуйте.

Оновлення: Хуррам запитує про ServletUtilsклас, на який посилається StaticServlet. Це просто клас з допоміжними методами, який я використовував для свого проекту. Єдиний потрібний вам метод coalesce(який ідентичний функції SQL COALESCE). Це код:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}

2
Не називайте свій внутрішній клас Помилка. Це може спричинити плутанину, оскільки ви можете помилити це за java.lang.Error Також чи ваш web.xml однаковий?
Леонель

Дякуємо за попередження про помилку web.xml - те саме, що "за замовчуванням" замінено на ім'я StaticServlet.
Бруно Де Фрейне

1
Що стосується методу coalesce, його можна замінити (всередині класу Servlet) на commons-lang StringUtils.defaultString (String, String)
Майк Мініцький

Метод transferStreams () також можна замінити Files.copy (є, os);
Герріт Брінк

Чому такий підхід настільки популярний? Чому люди повторно реалізують статичні файлові сервери, як це? Існує стільки дірок у безпеці, які лише чекають їх виявлення, і стільки функцій реальних статичних файлових серверів, які не реалізовані.
Леонгард Принтз

12

Судячи з наведеної вище прикладу, я вважаю, що вся ця стаття базується на помилковій поведінці в Tomcat 6.0.29 та новіших версіях. Див. Https://isissue.apache.org/bugzilla/show_bug.cgi?id=50026 . Оновлення до Tomcat 6.0.30 і поведінка між (Tomcat | Jetty) має зливатися.


1
Це теж моє розуміння svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. Нарешті, після маркування цього WONTFIX +3 років тому!
Бруно Де Фрайн

12

спробуйте це

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
    <url-pattern>*.css</url-pattern>
    <url-pattern>*.ico</url-pattern>
    <url-pattern>*.png</url-pattern>
    <url-pattern>*.jpg</url-pattern>
    <url-pattern>*.htc</url-pattern>
    <url-pattern>*.gif</url-pattern>
</servlet-mapping>    

Редагувати: Діє лише для специфікацій сервлета 2,5 і вище.


Здається, це не дійсна конфігурація.
Гедрокс

10

У мене була така ж проблема, і я вирішив її за допомогою коду "сервлета за замовчуванням" з бази даних коду Tomcat.

https://github.com/apache/tomcat/blob/master/java/org/apache/catalina/servlets/DefaultServlet.java

DefaultServlet це сервлет , який обслуговує статичні ресурси (JPG, HTML, CSS, GIF і т.д.) в Tomcat.

Цей сервлет дуже ефективний і має деякі властивості, які ви визначили вище.

Я думаю, що цей вихідний код - це хороший спосіб запустити та видалити функціональні можливості чи функції, які вам не потрібні.

  • Посилання на пакет org.apache.naming.resources можна видалити або замінити кодом java.io.File.
  • Посилання на пакет org.apache.catalina.util - це, можливо, лише корисні методи / класи, які можна дублювати у вашому вихідному коді.
  • Посилання на клас org.apache.catalina.Globals можна накреслити або видалити.

Здається, це залежить від багатьох речей org.apache.*. Як можна використовувати його з Jetty?
Бруно Де Фрейне

Ви маєте рацію, ця версія має занадто багато можливостей для Tomcat (і вона також підтримує багато речей, які ви, можливо, не хочете. Я відредагую свою відповідь.
Panagiotis Korros,


4

Я зробив це, розширивши tomcat DefaultServlet ( src ) і замінивши метод getRelativePath ().

package com.example;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.servlets.DefaultServlet;

public class StaticServlet extends DefaultServlet
{
   protected String pathPrefix = "/static";

   public void init(ServletConfig config) throws ServletException
   {
      super.init(config);

      if (config.getInitParameter("pathPrefix") != null)
      {
         pathPrefix = config.getInitParameter("pathPrefix");
      }
   }

   protected String getRelativePath(HttpServletRequest req)
   {
      return pathPrefix + super.getRelativePath(req);
   }
}

... І ось мої серветкові карти

<servlet>
    <servlet-name>StaticServlet</servlet-name>
    <servlet-class>com.example.StaticServlet</servlet-class>
    <init-param>
        <param-name>pathPrefix</param-name>
        <param-value>/static</param-value>
    </init-param>       
</servlet>

<servlet-mapping>
    <servlet-name>StaticServlet</servlet-name>
    <url-pattern>/static/*</url-pattern>
</servlet-mapping>  

1

Щоб обслуговувати всі запити програми Spring, а також /favicon.ico та файли JSP з / WEB-INF / jsp / *, що SpringUpplelBasedView Spring запитає, ви можете просто переустановити сервер jsp і сервлет за замовчуванням:

  <servlet>
    <servlet-name>springapp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>/WEB-INF/jsp/*</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/favicon.ico</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>springapp</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

Ми не можемо покластися на * .jsp URL-шаблон для стандартного відображення сервлета jsp, оскільки шаблон шляху / / * узгоджується до того, як перевіряється будь-яке відображення розширення. Зображення сервлета jsp у більш глибокій папці означає, що він спочатку відповідає. Узгодження "/favicon.ico" відбувається саме перед узгодженням шаблону шляху. Працюватимуть більш глибокі збіги шляху або точні збіги, але жодні збіги розширень не зможуть пропустити відповідність шляху / / *. Зображення '/' до сервлета за замовчуванням, здається, не працює. Ви можете подумати, що точне "/" переможе шаблон "/ *" на springapp.

Вищевказане рішення фільтра не працює для переадресованих / включених запитів JSP з програми. Щоб він працював, мені довелося застосувати фільтр до springapp безпосередньо, і в цей момент відповідність шаблону URL-адреси була марною, оскільки всі запити, що надходять до програми, також переходять до її фільтрів. Тому я додав у фільтр відповідність шаблонів, а потім дізнався про сервлет 'jsp' і побачив, що він не видаляє префікс шляху, як це робить сервлет за замовчуванням. Це вирішило мою проблему, яка була не зовсім однаковою, але досить поширеною.


1

Перевірено на Tomcat 8.x: статичні ресурси справні, якщо карта кореневих сервлетів до "". Для сервлета 3.x це можна зробити@WebServlet("")


0

Використовуйте org.mortbay.jetty.handler.ContextHandler. Вам не потрібні додаткові компоненти, такі як StaticServlet.

У пристані,

$ cd контексти

$ cp javadoc.xml static.xml

$ vi static.xml

...

<Configure class="org.mortbay.jetty.handler.ContextHandler">
<Set name="contextPath">/static</Set>
<Set name="resourceBase"><SystemProperty name="jetty.home" default="."/>/static/</Set>
<Set name="handler">
  <New class="org.mortbay.jetty.handler.ResourceHandler">
    <Set name="cacheControl">max-age=3600,public</Set>
  </New>
 </Set>
</Configure>

Встановіть значення contextPath за допомогою префікса URL-адреси та встановіть значення resourceBase як шлях до файлу статичного вмісту.

Це працювало для мене.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.