静的コンテンツを提供するためのサーブレット


145

2つの異なるコンテナー(TomcatとJetty)にWebアプリケーションをデプロイしますが、静的コンテンツを提供するためのデフォルトのサーブレットでは、使用したいURL構造を処理する方法が異なります(詳細)。

したがって、Webアプリケーションに小さなサーブレットを含めて、独自の静的コンテンツ(画像、CSSなど)を提供することを検討しています。サーブレットには次のプロパティが必要です。

  • 外部依存関係なし
  • シンプルで信頼できる
  • If-Modified-Sinceヘッダーのサポート(つまり、カスタムgetLastModifiedメソッド)
  • (オプション)gzipエンコーディング、etagsなどのサポート

そのようなサーブレットはどこかで利用できますか?私が見つけることができる最も近いものは、サーブレットブックの例4-10です。

更新:私が使用したい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に基づいて、独自にローリングすることは、簡単な作業のようです。私は何度も自分でやってきました...
Stu Thompson

質問を編集して、URL構造を詳しく説明しました。そして、はい、私は自分のサーブレットをローリングしてしまいました。以下の私の答えを参照してください。
Bruno De Fraine 2008

1
静的コンテンツにWebサーバーを使用しないのはなぜですか?
スティーブン、

4
@Stephen:Tomcat / Jettyの前に常にApacheがあるとは限らないため。また、個別の設定の煩わしさを回避するために。しかし、あなたは正しい、私はそのオプションを検討することができます。
Bruno De Fraine、2008年

なぜこの<servlet-mapping> <servlet-name> default </ 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
実際には、
servlet

5
サーブレット2.5以降では、servlet-mapping内の複数のurl-patternタグをサポートしています
vivid_voidgroup

インデックスファイル(index.html)はサーブレットよりも優先される可能性があるため、注意が必要です。
Andres

私はそれを使用するのは悪い考えだと思います*.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);
    }
}

この質問には、/をコントローラーに、/ staticを静的コンテンツに、フィルターを使用して適切にマッピングする方法があります。受け入れられたものの後upvoted答えをチェック:stackoverflow.com/questions/870150/...
デヴィッドCarboniの


30

FileServletは、ほとんどすべてのHTTP(etags、chunkingなど)をサポートしているため、良い結果が得られました。


ありがとう!何時間にもわたる試みの失敗と悪い答え、そしてこれが私の問題を解決しました
Yossi Shasho

4
アプリ外のフォルダーからコンテンツを提供するために(私はそれを使用して、ディスクからフォルダーを処理するために使用します(例:C:\ resources))、この行を変更しました:this.basePath = getServletContext()。getRealPath(getInitParameter( "basePath ")); それを次のように置き換えます。this.basePath = getInitParameter( "basePath");
Yossi Shasho 2012

1
更新されたバージョンは、showcase.omnifaces.org
servlets / FileServletで

26

静的リソースサーブレットの抽象テンプレート

部分的に基づいて、このブログ 2007年から、ここで適切にキャッシュを扱う、サーブレットの近代化と再利用性の高い抽象テンプレートだETagIf-None-MatchIf-Modified-Since(ただし、gzipとレンジのサポート、ちょうどそれをシンプルに保つために、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に従ってメソッドを実装することだけです。

ファイルシステムから提供される具体的な例:

/files/foo.extローカルディスクファイルシステムなどからURLを介してサービスを提供する具体的な例を次に示します。

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

}

データベースから提供される具体的な例:

以下に/files/foo.extbyte[] contentプロパティを持つエンティティを返すEJBサービス呼び出しを介して、データベースからのようなURLを介してサービスを提供する具体的な例を示します。

@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 2018

これは賛成票を得るべきではありません。このようなサーブレットを使用してWebページの静的ファイルを提供することは、災害セキュリティの賢明なレシピです。このような問題はすべて解決済みであり、未発見のセキュリティ時限爆弾が発生する可能性が高い新しいカスタム方法を実装する理由はありません。正しいパスは、コンテンツを提供するようにTomcat / GlassFish / Jettyなどを設定するか、NGinXなどの専用ファイルサーバーを使用することです。
Leonhard Printz

@LeonhardPrintz:セキュリティの問題を指摘したら、答えを削除し、Tomcatの友達に報告します。問題ない。
BalusC

19

結局自分で転がしてしまいましたStaticServletIf-Modified-Since、gzipエンコーディングをサポートし、warファイルから静的ファイルも提供できるはずです。それほど難しいコードではありませんが、完全に簡単なものでもありません。

コードが利用可能です:StaticServlet.java。コメントしてください。

更新: Khurram ServletUtilsがで参照されているクラスについて尋ねStaticServletます。これは、プロジェクトで使用した補助メソッドを含むクラスです。必要な唯一の方法はcoalesce(SQL関数と同じですCOALESCE)です。これはコードです:

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

2
内部クラスにErrorという名前を付けないでください。java.lang.Errorと間違える可能性があるため、混乱が生じる可能性があります。また、web.xmlは同じですか?
レオネル

エラー警告をありがとう。web.xmlも同じで、 "default"はStaticServletの名前に置き換えられています。
Bruno De Fraine

1
合体メソッドについては、(サーブレットクラス内で)commons-lang StringUtils.defaultString(String、String)に置き換えることができます
Mike Minicki

transferStreams()メソッドは、Files.copy(is、os)で置き換えることもできます。
Gerrit Brink 2014

このアプローチがなぜそれほど人気が​​あるのですか?なぜ人々はこのように静的ファイルサーバーを再実装するのですか?発見されるのを待っているだけの非常に多くのセキュリティホールがあり、実装されていない実際の静的ファイルサーバーの多くの機能があります。
Leonhard Printz

12

上記の例の情報から判断すると、この記事全体はTomcat 6.0.29以前のバグのある動作に基づいていると思います。https://issues.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年前にマークした後!
Bruno De Fraine

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仕様以上でのみ有効です。


これは有効な構成ではないようです。
Gedrox、2012年

10

同じ問題があり、Tomcatコードベースの「デフォルトサーブレット」のコードを使用して解決しました。

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

デフォルトサーブレットは、 Tomcatの静的リソース(JPG、HTML、CSS、GIFなど)役割を果たすサーブレットです。

このサーブレットは非常に効率的で、上で定義したいくつかのプロパティがあります。

このソースコードは、不要な機能や依存関係を開始および削除するための良い方法だと思います。

  • org.apache.naming.resourcesパッケージへの参照は削除するか、java.io.Fileコードで置き換えることができます。
  • org.apache.catalina.utilパッケージへの参照は、おそらくソースコードで複製できるユーティリティメソッド/クラスのみです。
  • org.apache.catalina.Globalsクラスへの参照はインライン化または削除できます。

それはの多くのものに依存しているようですorg.apache.*。Jettyでどのように使用できますか?
Bruno De Fraine 2008

あなたは正しいです、このバージョンは、(Tomcatにあまりにも多くのdepedenciesを持っているが、それはまた、あなたが望んでいないかもしれない多くのことをサポートしていcaand私は私の答えを編集します。。
パナギオティスKorros


4

これを行うには、tomcat DefaultServletsrc)を拡張し、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および/ WEB-INF / jsp / *からのJSPファイルを提供するには、SpringのAbstractUrlBasedViewがリクエストする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サーブレットの標準マッピングの* .jsp url-patternに依存することはできません。jspサーブレットをより深いフォルダにマッピングすると、最初に一致します。パスパターンマッチングの前に、「/ favicon.ico」のマッチングが正確に行われます。より深いパスの一致、または完全な一致は機能しますが、拡張子の一致が '/ *'パスの一致を超えることはありません。'/'をデフォルトのサーブレットにマッピングしても機能しないようです。正確な「/」は、springappの「/ *」パスパターンに勝ると思います。

上記のフィルターソリューションは、アプリケーションからの転送またはインクルードされたJSPリクエストに対しては機能しません。これを機能させるために、springappに直接フィルターを適用する必要がありました。その時点で、アプリケーションに送信されるすべての要求もそのフィルターに送信されるため、url-patternマッチングは役に立たなくなりました。そのため、フィルターにパターンマッチングを追加し、 '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の値を静的コンテンツのファイルパスとして設定します。

それは私のために働いた。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.