Filterとは
Servlet FilterはJava Servletの一部で、リクエストやレスポンスをインターセプト(横取り)して何らかの処理を行うことができる機能を提供します。
この機能を使うと、特定の共通の処理をFilterに書くだけで全てのサーブレットに適用することができます。定型コードを書く必要がなくなります。さらには、Webアプリケーションの機能を拡張することもできます。
日本語文字化け対策のFilter
文字化け対策のFilterは以下のとおりです。urlPatterns = {"/*"}となっていることですべてのURLパターンに対してこのエンコーディング設定が適用されます
//入出力関連のクラスやインターフェースを提供するjava.ioパッケージをインポートします
import java.io.IOException;
//サーブレットのクラスやインターフェースを提供するjavax.servletパッケージをインポートします
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
//WebFilterアノテーションを使用して、このフィルタの名前とフィルタリング対象のURLパターンを指定します。“/*”は全てのリクエストをフィルタするという意味になります
@WebFilter(filterName = "EncodingFilter", urlPatterns = { "/*" })
//EncodingFilterクラスを宣言し、Filterインターフェースを実装します。これにより、このクラスはサーブレットフィルタとして動作します
public class EncodingFilter implements Filter {
// 文字エンコーディングを指定するための定数を宣言します。このクラスでは "UTF-8" が指定されています
private static final String encoding = "UTF-8";
// クライアントからのリクエスト時にフィルタとして動作するメソッドです
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
throws IOException, ServletException {
// リクエストの文字エンコーディングがnullの場合、"UTF-8" をセットします
if (null == request.getCharacterEncoding()) {
request.setCharacterEncoding(encoding);
}
// レスポンスの文字エンコーディングを "UTF-8" に設定します
response.setCharacterEncoding(encoding);
// フィルタチェーンの次のフィルタまたはサーブレットにリクエストとレスポンスを送信します
next.doFilter(request, response);
}
}
JavaスクリプトのサニタイズをするFilter
Webアプリケーションには入力フォームにJavaScriptを挿入されて実行されてしまうという脆弱性があります。XSS(クロスサイトスクリプティング: 【cross site scripting】)と呼ばれる大変有名な攻撃手法です。
おそらく最近もXSSの被害事例があると思いますのでリンクをたどってみてください。
以下の簡単なWebアプリケーションを例にとって説明します。
入力フォームjapanese_form.htmlです。
<!DOCTYPE html>
<html>
<head>
<title>japanese_form.html</title>
<meta charset="UTF-8">
</head>
<body>
<form action="/03_JavaWebText/Japanese">
<input type="text" name="name"/><br>
<button type="submit">送信</button>
</form>
</body>
</html>
次にControllerとしてのServletです。
package p04;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = {"/Japanese"})
public class Japanese extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name = request.getParameter("name");
request.setAttribute("name", name);
request.getRequestDispatcher("/04Form/japanese.jsp").forward(request, response);
}
}
最後にViewとしてのJSPです。
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset=UTF-8">
<title>japanese.jsp</title>
</head>
<body>
<h1>こんにちは${requestScope.name}さん</h1>
</body>
</html>
ここで、入力フォームの入力欄に次のようなJavaScriptを入力してみてください。
<script>alert("いけない情報");</script>
すると下図4.7のようなダイアログボックスが表示されたと思います。
HTMLのソースを見ると次のようになっています。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>japanese.jsp</title>
</head>
<body>
<h1>こんにちは<script>alert("いけない情報");</script>さん</h1>
</body>
</html>
詳しくは述べませんが、入力フォームに流し込んだJavaScriptが実行できるということは、このWebアプリケーションではJavaScriptでできることは何でもできるということです。JavaScriptが実行されるということはXSS攻撃にさらされる可能性があるということです。(とは言っても研修で使用する最新のブラウザですとほとんど問題ありませんが)
どうしたら良いのでしょうか?
出力先のJSPを次のようにしてみてください。
<!DOCTYPE html>
<html>
<head>
<title>japanese_form2.html</title>
<meta charset="UTF-8">
</head>
<body>
<form action="/03_JavaWebText/Japanese2">
<input type="text" name="name"/><br>
<button type="submit">送信</button>
</form>
</body>
</html>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset=UTF-8">
<title>japanese2.jsp</title>
</head>
<body>
<h1>こんにちは<c:out value="${name}" />さん</h1>
</body>
</html>
- <japanese1.jsp> との違いを2点挙げてみてください。
あなたの答え: |
2行目で読み込んでいるのはJSTLのコアタグというものです。JSPでいろいろな処理を可能にしてくれるものです。今回の<c:out>は出力を担います。
上記の実行結果は下図4.8のとおりになりました。
HTMLのソースを見ると以下のようになっています。(抜粋)
<h1>こんにちは<script>alert("いけない情報");</script>さん</h1>
つまり<c:out>が、「<を<(【less than】の意味)」、「>を>(【greater than】の意味)」のようにタグを変換してくれたのです。
なお、 < や > を文字実体参照と呼びます。
このようにプログラムでサニタイズ【sanitize】とは、JavaScriptなどの特別な意味を持つ可能性のある文字列を置き換えて無効化することをいいます。サニタイズとはもともと消毒という意味の英語です。Java以外の言語にもサニタイズの仕組みが備わっていますので、Webアプリケーションを作成する際は気をつけましょう。
このように<c:out>タグを使えばXSS対策が可能なのですが、人は忘れやすくうっかりしやすいものです。
Java Servletフィルターを使用すれば、全ての入力フォームに対してユーザーからの入力をサニタイズ(不正な文字や悪意のあるコードを取り除く)することができます。次の例は、ユーザーからの入力からHTMLとJavaScriptのタグをエスケープするフィルターです。
なお、以下のコードを実行するにはApache Commons Textのjarファイルが必要です。
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
// HTTPリクエストをラップするクラスを提供するjavax.servlet.httpパッケージをインポートします
import javax.servlet.http.HttpServletRequestWrapper;
// 文字列操作のユーティリティを提供するApache Commons Textパッケージをインポートします
import org.apache.commons.text.StringEscapeUtils;
@WebFilter(filterName = "XSSFilter", urlPatterns = {"/*"})
public class XSSFilter implements Filter {
// クライアントからのリクエスト時にフィルタとして動作するメソッドです
public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
throws IOException, ServletException {
// 元のリクエストをラップした新しいリクエストを作成します
XSSRequestWrapper wrappedRequest = new XSSRequestWrapper((HttpServletRequest) request);
// フィルタチェーンの次のフィルタまたはサーブレットにラップしたリクエストとレスポンスを送信します
next.doFilter(wrappedRequest, response);
}
// XSS攻撃から保護するためのリクエストラッパークラスを定義します
private static class XSSRequestWrapper extends HttpServletRequestWrapper {
// コンストラクタで元のリクエストを引数に取り、スーパークラスのコンストラクタに渡します
public XSSRequestWrapper(HttpServletRequest servletRequest) {
super(servletRequest);
}
// HttpServletRequestWrapperクラスのgetParameterValuesメソッドをオーバーライドします。
// このメソッドは、指定されたパラメータの全ての値を取得します
@Override
public String[] getParameterValues(String parameter) {
// スーパークラスの同名メソッドを呼び出し、パラメータの値を取得します
String[] values = super.getParameterValues(parameter);
// 取得した値がnullであれば、nullを返します
if (values == null) {
return null;
}
// 取得した値の数を数えます
int count = values.length;
// エスケープ後の値を格納する配列を準備します
String[] encodedValues = new String[count];
// 各値に対してエスケープ処理を行い、結果を配列に格納します
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(values[i]);
}
// エスケープ後の値を格納した配列を返します
return encodedValues;
}
// HttpServletRequestWrapperクラスのgetParameterメソッドをオーバーライドします。
// このメソッドは、指定されたパラメータの値を取得します
@Override
public String getParameter(String parameter) {
// スーパークラスの同名メソッドを呼び出し、パラメータの値を取得します
String value = super.getParameter(parameter);
// 取得した値をエスケープ処理し、結果を返します
return stripXSS(value);
}
// XSS攻撃から保護するためのエスケープ処理を行います
private String stripXSS(String value) {
// 値がnullでなければエスケープ処理を行います
if (value != null) {
// HTMLエンティティに対するエスケープ処理を行います
value = StringEscapeUtils.escapeHtml4(value);
// EcmaScript(JavaScript)に対するエスケープ処理を行います
value = StringEscapeUtils.escapeEcmaScript(value);
}
// エスケープ後の値を返します
return value;
}
}
}
ログインチェックのFilter
このコードでは、セッションからユーザー情報を取得してログイン状態を確認します。ユーザーがログインしている、またはログインページへのアクセスの場合はリクエストを次に進めます。それ以外の場合、つまりユーザーがログインしておらず、かつ非ログインページへのアクセスを試みている場合にはログインページへリダイレクトします。
なお、ログインチェックのFilterはシステム開発時には外しておいたほうが開発が楽になるでしょう。
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebFilter(filterName = "AuthenticationFilter", urlPatterns = {"/*"})
public class AuthenticationFilter implements Filter {
// リクエストとレスポンスがフィルターを通過する際に呼び出されるメソッド
public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
throws IOException, ServletException {
// ServletRequestをHttpServletRequestにダウンキャストします
HttpServletRequest httpRequest = (HttpServletRequest) request;
// ServletResponseをHttpServletResponseにダウンキャストします
HttpServletResponse httpResponse = (HttpServletResponse) response;
// セッションを取得します。存在しない場合はnullを返します
HttpSession session = httpRequest.getSession(false);
// セッションが存在し、それが"user"という名前の属性を持つ場合、ユーザーはログインしていると見なします
boolean isLoggedIn = (session != null && session.getAttribute("user") != null);
// ログインのリクエストURIを定義します
String loginURI = httpRequest.getContextPath() + "/login";
// 現在のリクエストがログインリクエストであるか確認します
boolean isLoginRequest = httpRequest.getRequestURI().equals(loginURI);
// 現在のリクエストがログインページであるか確認します
boolean isLoginPage = httpRequest.getRequestURI().endsWith("login.jsp");
// ユーザーがすでにログインしていて、ログインリクエストまたはログインページにアクセスしようとしている場合
if (isLoggedIn && (isLoginRequest || isLoginPage)) {
// ホームページに転送します
RequestDispatcher dispatcher = httpRequest.getRequestDispatcher("/home");
dispatcher.forward(request, response);
} else if (isLoggedIn || isLoginRequest) {
// ユーザーがログインしているか、ログインリクエストの場合、リクエストをフィルターチェーンに進めます
next.doFilter(request, response);
} else {
// ユーザーがログインしていない場合、ログインページにリダイレクトします
httpResponse.sendRedirect(loginURI);
}
}
}
jspの直打ちを禁止するFilter
以下のFilterは直接JSPにアクセスすることを禁止するためのものです。リクエストURLがJSPファイルを指しているかどうかをチェックし、そのような場合にはエラーページにリダイレクトします。
このコードは、リクエストURIが".jsp"で終わるもの(つまりJSPファイルへの直接アクセス)を検出すると、HTTPステータスコード404(Not Found)を送信します。それ以外のリクエストは次へ進みます。
なお、jspの直打ちを禁止するFilterはシステム開発時には外しておいたほうが開発が楽になるでしょう。
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebFilter(filterName = "JSPFilter", urlPatterns = {"/*"})
public class JSPFilter implements Filter {
// リクエストとレスポンスがフィルターを通過する際に呼び出されるメソッド
public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
throws IOException, ServletException {
// ServletRequestをHttpServletRequestにダウンキャストします
HttpServletRequest httpRequest = (HttpServletRequest) request;
// ServletResponseをHttpServletResponseにダウンキャストします
HttpServletResponse httpResponse = (HttpServletResponse) response;
// リクエストのURIを取得します
String requestUri = httpRequest.getRequestURI();
// リクエストURIが".jsp"で終わる場合、つまりJSPファイルが直接リクエストされた場合
if (requestUri.endsWith(".jsp")) {
// 404エラーを送信します。
httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
} else {
// ".jsp"で終わらないURIについては、フィルターチェーンを続行します
next.doFilter(request, response);
}
}
}
エラーが起こった場合にエラーページに遷移させるFilter
エラーページをそのままユーザーに表示すると、システムの詳細情報(例えば、スタックトレース)がユーザーに公開されることになり、セキュリティのリスクとなる場合があります。なぜなら、攻撃者がシステムの脆弱性を見つけ出す手がかりを入手する可能性があるからです。
そのため、以下のコードでは、doFilter
メソッドの実行中に例外がスローされた場合、その例外のメッセージをリクエスト属性に追加し、エラーページ(この例では"/error.jsp")にフォワードします。これにより、エラーページで一般的なエラーメッセージを表示させています。
なお、エラーページに遷移させるFilterはシステム開発時には外しておいたほうが開発が楽になるでしょう。
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebFilter(filterName = "ErrorHandlingFilter", urlPatterns = {"/*"})
public class ErrorHandlingFilter implements Filter {
// リクエストとレスポンスがフィルターを通過する際に呼び出されるメソッド
public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
throws IOException, ServletException {
try {
// 次のフィルターまたはサーブレットにリクエストとレスポンスを渡します
next.doFilter(request, response);
} catch (Exception e) {
// リクエストやレスポンス処理中に例外が発生した場合の処理を記述します
// ServletRequestをHttpServletRequestにダウンキャストします
HttpServletRequest httpRequest = (HttpServletRequest) request;
// ServletResponseをHttpServletResponseにダウンキャストします
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 例外のメッセージをリクエスト属性に追加します
httpRequest.setAttribute("errorMessage", e.getMessage());
// エラーページへのディスパッチャーを取得します(ここでは"/error.jsp"をエラーページと仮定します)
RequestDispatcher dispatcher = httpRequest.getRequestDispatcher("/error.jsp");
// エラーページにリクエストとレスポンスを転送します
dispatcher.forward(httpRequest, httpResponse);
}
}
}
ブラウザキャッシュを無効化するFilter
ブラウザキャッシュは便利な半面、開発中は実施した更新が反映されずに困ることもあります。
HTTPヘッダの Cache-Control ディレクティブを使用することで、ブラウザにキャッシュの振る舞いを指示することができます。
以下に一部の主要なディレクティブを示します:
no-store: このディレクティブは、ブラウザにこの応答を一切キャッシュしないように指示します。これは機密情報が含まれている場合などに有用です。
no-cache: このディレクティブは、ブラウザにキャッシュを利用する前にサーバーに再確認を行うように指示します。これにより、サーバーが最新のバージョンを提供することが確実になります。
private: このディレクティブは、レスポンスが特定のユーザー専用であり、共有キャッシュには保存しないように指示します。
public: このディレクティブは、通常はキャッシュされないようなレスポンスでも、キャッシュしてよいと指示します。
max-age=<秒数>: このディレクティブは、リソースが古くなるまでの最大時間を秒単位で指示します。
これらのディレクティブを組み合わせることも可能です。例えば、Cache-Control: no-store, no-cache はブラウザにキャッシュを一切保存しないように指示し、さらにキャッシュを利用する前に再確認を行うように指示します。
以下のフィルタはブラウザキャッシュを無効化します。
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
@WebFilter(filterName = "CacheControlFilter", urlPatterns = { "/*" })
public class CacheControlFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
res.setHeader("Pragma", "no-cache"); // HTTP 1.0.
res.setDateHeader("Expires", 0); // Proxies.
chain.doFilter(request, response);
}
}
上記のフィルターはすべてのレスポンスに対してブラウザキャッシュを無効にする設定となっています。開発環境では、最新のコードの動作を確認するためにキャッシュを無効にすることがよくあります。しかし、本番環境ではこれが最善のアプローチとは限りません。
キャッシュはパフォーマンスを向上させ、サーバーの負荷を軽減するために重要です。たとえば、スタイルシートやスクリプト、画像などの静的ファイルは頻繁に変更されないことが多いため、これらのファイルをキャッシュすることで、ページの読み込み時間を短縮し、ユーザー体験を向上させることができます。
その一方で、動的に生成されるコンテンツやユーザー固有の情報を表示するページなどは、キャッシュすると古い情報が表示される可能性があるため、キャッシュを制限または無効にしたい場合もあります。
その場合は、以下のようにurlPatternsパスを指定することで特定のパスのキャッシュだけをコントロールすることもできます。
@WebFilter(urlPatterns={"/images/*", "/css/*", "/js/*"})
public class StaticResourceCacheControlFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Cache-Control", "public, max-age=31536000"); // 1 year
chain.doFilter(req, res);
}
}
上記フィルタは、/images/*
、/css/*
、/js/*
のパスに一致するリクエストに対して、キャッシュを許可し、キャッシュの最大寿命を1年に設定しています。