ログアウトの問題を適切かつエレガントに解決する

多くのWebアプリケーションには、銀行口座番号やクレジットカードデータなどの過度に機密性の高い個人情報は含まれていません。ただし、一部のデータには、何らかのパスワード保護スキームを必要とする機密データが含まれています。たとえば、労働者がタイムシート情報の入力、トレーニングコースへのアクセス、時間料金の確認などにWebアプリケーションを使用する必要がある工場では、SSL(Secure Socket Layer)を使用するのはやり過ぎです(SSLページはキャッシュされません。 SSLの説明は、この記事の範囲を超えています)。しかし確かに、これらのアプリケーションには何らかのパスワード保護が必要です。そうしないと、労働者(この場合はアプリケーションのユーザー)は、すべての工場従業員に関する機密情報を発見することになります。

上記の状況と同様の例として、公共図書館、病院、インターネットカフェにあるインターネットを備えたコンピューターがあります。ユーザーがいくつかの共通のコンピューターを共有するこの種の環境では、ユーザーの個人データを保護することが重要です。同時に、適切に設計および実装されたアプリケーションは、ユーザーについて何も想定せず、最小限のトレーニングしか必要としません。

完璧なWebアプリケーションが完璧な世界でどのように動作するかを見てみましょう。ユーザーがブラウザーでURLを指定します。 Webアプリケーションは、ユーザーに有効な資格情報の入力を求めるログインページを表示します。彼女はユーザーIDとパスワードを入力します。提供された資格情報が正しいと仮定すると、認証プロセスの後、Webアプリケーションにより、ユーザーは自分の許可された領域に自由にアクセスできます。終了する時間になったら、ユーザーはページのログアウトボタンを押します。 Webアプリケーションは、ユーザーが本当にログアウトしたいかどうかを確認するように求めるページを表示します。彼女が[OK]ボタンを押すと、セッションが終了し、Webアプリケーションは別のログインページを表示します。これで、ユーザーは、他のユーザーが自分の個人データにアクセスすることを心配することなく、コンピューターから離れることができます。別のユーザーが同じコンピューターの前に座っています。彼は戻るボタンを押します。Webアプリケーションは、最後のユーザーのセッションのページを表示してはなりません。実際、Webアプリケーションは、2番目のユーザーが有効な資格情報を提供するまで、ログインページを常にそのままにしておく必要があります。そうしないと、ユーザーは自分の許可された領域にアクセスできなくなります。

この記事では、サンプルプログラムを通じて、Webアプリケーションでこのような動作を実現する方法を示します。

JSPサンプル

ソリューションを効率的に説明するために、この記事では、WebアプリケーションlogoutSampleJSP1で発生した問題を示すことから始めます。このサンプルアプリケーションは、ログアウトプロセスを適切に処理しないさまざまなWebアプリケーションを表しています。 logoutSampleJSP1は、以下のJSP(JavaServer Pagesの)ページで構成されていますlogin.jsphome.jspsecure1.jspsecure2.jsplogout.jsploginAction.jsp、とlogoutAction.jsp。 JSPページhome.jspsecure1.jspsecure2.jsp、およびlogout.jsp認証されていないユーザに対して保護されていますが、つまり、彼らは安全な情報が含まれており、いずれかで、またはユーザがログアウト後にユーザーがログインする前にブラウザに表示されることはありません。このページにlogin.jspは、ユーザーがユーザー名とパスワードを入力するフォームが含まれています。ページlogout.jspユーザーが実際にログアウトすることを確認するように求めるフォームが含まれています。JSPページloginAction.jsplogoutAction.jspはコントローラーとして機能し、ログインアクションとログアウトアクションをそれぞれ実行するコードを含みます。

2番目のサンプルWebアプリケーションであるlogoutSampleJSP2は、logoutSampleJSP1の問題を解決する方法を示しています。ただし、logoutSampleJSP2には問題が残ります。ログアウトの問題は、特別な状況下でも明らかになる可能性があります。

3番目のサンプルWebアプリケーションであるlogoutSampleJSP3は、logoutSampleJSP2を改良し、ログアウトの問題に対する許容可能なソリューションを表しています。

最後のサンプルWebアプリケーションlogoutSampleStrutsは、JakartaStrutsがログアウトの問題をエレガントに解決する方法を示しています。

注:この記事に付属するサンプルは、最新のMicrosoft Internet Explorer(IE)、Netscape Navigator、Mozilla、FireFox、およびAvantブラウザー用に作成およびテストされています。

ログインアクション

Brian Pontarelliの優れた記事「J2EEセキュリティ:コンテナとカスタム」では、さまざまなJ2EE認証アプローチについて説明しています。結局のところ、HTTPの基本認証とフォームベースの認証アプローチは、ログアウトを処理するためのメカニズムを提供していません。したがって、解決策は、最も柔軟性が高いカスタムセキュリティ実装を採用することです。

カスタム認証アプローチの一般的な方法は、フォーム送信からユーザー資格情報を取得し、LDAP(ライトウェイトディレクトリアクセスプロトコル)やRDBMS(リレーショナルデータベース管理システム)などのバックエンドセキュリティレルムと照合することです。指定された資格情報が有効な場合、ログインアクションはオブジェクトにオブジェクトを保存しHttpSessionます。このオブジェクトの存在はHttpSession、ユーザーがWebアプリケーションにログインしたことを示します。わかりやすくするために、付随するすべてのサンプルアプリケーションHttpSessionは、ユーザーがログインしていることを示すためにユーザー名文字列のみをに保存します。リスト1はloginAction.jsp、ログインアクションを説明するためにページに含まれるコードスニペットを示しています。

リスト1

// ... // RequestDispatcherオブジェクトを初期化します;デフォルトでホームページに転送するように設定しますRequestDispatcherrd = request.getRequestDispatcher( "home.jsp"); //接続とステートメントを準備しますrs = stmt.executeQuery( "select password from USER where userName = '" + userName + "'"); if(rs.next()){//クエリは結果セットに1つのレコードのみを返します。 userNameごとに1つのパスワードのみ。これは主キーでもありますif(rs.getString( "password")。equals(password)){//有効なパスワードの場合session.setAttribute( "User"、userName); //ユーザー名文字列をセッションオブジェクトに保存します} else {//パスワードが一致しません。つまり、無効なユーザーパスワードrequest.setAttribute( "Error"、 "Invalid password。"); rd = request.getRequestDispatcher( "login.jsp"); }} //結果セットにレコードがありません。つまり、無効なユーザー名else {request.setAttribute( "Error"、 "無効なユーザー名。"); rd = request.getRequestDispatcher( "login.jsp"); }} //コントローラーとして、loginAction.jspは最終的に「login.jsp」または「home.jsp」に転送します。rd.forward(request、response); //..。

これと付随するサンプルWebアプリケーションの残りの部分では、セキュリティレルムはRDBMSであると想定されています。ただし、この記事の概念は透過的であり、あらゆるセキュリティ領域に適用できます。

ログアウトアクション

ログアウトアクションには、ユーザー名文字列を削除invalidate()し、ユーザーのHttpSessionオブジェクトのメソッドを呼び出すだけです。リスト2はlogoutAction.jsp、ログアウトアクションを説明するためにページに含まれているコードスニペットを示しています。

リスト2

// ... session.removeAttribute( "User"); session.invalidate(); //..。

保護されたJSPページへの認証されていないアクセスを防止する

要約すると、フォーム送信から取得した資格情報の検証が成功すると、ログインアクションは単にユーザー名文字列をHttpSessionオブジェクトに配置します。ログアウトアクションは反対のことをします。からユーザー名文字列を削除し、オブジェクトのメソッドHttpSessionを呼び出します。ログインアクションとログアウトアクションの両方を意味のあるものにするために、保護されたすべてのJSPページは、最初にに含まれるユーザー名文字列をチェックして、ユーザーが現在ログインしているかどうかを判断する必要があります。 Webアプリケーションは、JSPページの残りの部分の動的コンテンツをブラウザに送信します。それ以外の場合、JSPページは制御フローをログインページに転送します。 JSPページ、、invalidate()HttpSessionHttpSessionHttpSessionlogin.jsphome.jspsecure1.jspsecure2.jsp、およびlogout.jspすべてに、リスト3に示すコードスニペットが含まれています。

リスト3

// ... String userName =(String)session.getAttribute( "User"); if(null == userName){request.setAttribute( "Error"、 "セッションが終了しました。ログインしてください。"); RequestDispatcher rd = request.getRequestDispatcher( "login.jsp"); rd.forward(リクエスト、レスポンス); } // ... //このJSPの残りの動的コンテンツをブラウザに提供できるようにする// ...

このコードスニペットは、からユーザー名文字列を取得しますHttpSession。取得したユーザー名文字列がnullの場合、Webアプリケーションは、「セッションが終了しました。ログインしてください。」というエラーメッセージを表示して制御フローをログインページに転送することで中断します。それ以外の場合、Webアプリケーションは、保護されたJSPページの残りの部分を通常のフローで通過できるため、そのJSPページの動的コンテンツを提供できます。

logoutSampleJSP1を実行しています

logoutSampleJSP1を実行すると、次の動作が発生します。

  • The application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served if the user has not logged in. In other words, assuming the user has not logged in but points the browser to those JSP pages' URLs, the Web application forwards the control flow to the login page with the error message "Session has ended. Please log in.".
  • Likewise, the application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served after the user has already logged out. In other words, after the user has already logged out, if he points the browser to the URLs of those JSP pages, the Web application will forward the control flow to the login page with the error message "Session has ended. Please log in.".
  • The application does not behave correctly if, after the user has already logged out, he clicks on the Back button to navigate back to the previous pages. The protected JSP pages reappear on the browser even after the session has ended (with the user logging out). However, continual selection of any link on these pages brings the user to the login page with the error message "Session has ended. Please log in.".

Prevent the browsers from caching

The root of the problem is the Back button that exists on most modern browsers. When the Back button is clicked, the browser by default does not request a page from the Web server. Instead, the browser simply reloads the page from its cache. This problem is not limited to Java-based (JSP/servlets/Struts) Web applications; it is also common across all technologies and affects PHP-based (Hypertext Preprocessor), ASP-based, (Active Server Pages), and .Net Web applications.

After the user clicks on the Back button, no round trip back to the Web servers (generally speaking) or the application servers (in Java's case) takes place. The interaction occurs among the user, the browser, and the cache. So even with the presence of Listing 3's code in the protected JSP pages such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp, this code never gets the chance to execute when the Back button is clicked.

Depending on whom you ask, the caches that sit between the application servers and the browsers can either be a good thing or a bad thing. These caches do in fact offer a few advantages, but that's mostly for static HTML pages or pages that are graphic- or image-intensive. Web applications, on the other hand are more data-oriented. As data in a Web application is likely to change frequently, it is more important to display fresh data than save some response time by going to the cache and displaying stale or out-of-date information.

Fortunately, the HTTP "Expires" and "Cache-Control" headers offer the application servers a mechanism for controlling the browsers' and proxies' caches. The HTTP Expires header dictates to the proxies' caches when the page's "freshness" will expire. The HTTP Cache-Control header, which is new under the HTTP 1.1 Specification, contains attributes that instruct the browsers to prevent caching on any desired page in the Web application. When the Back button encounters such a page, the browser sends the HTTP request to the application server for a new copy of that page. The descriptions for necessary Cache-Control headers' directives follow:

  • no-cache: forces caches to obtain a new copy of the page from the origin server
  • no-store: directs caches not to store the page under any circumstance

For backward compatibility to HTTP 1.0, the Pragma:no-cache directive, which is equivalent to Cache-Control:no-cache in HTTP 1.1, can also be included in the header's response.

By leveraging the HTTP headers' cache directives, the second sample Web application, logoutSampleJSP2, that accompanies this article remedies logoutSampleJSP1. logoutSampleJSP2 differs from logoutSampleJSP1 in that Listing 4's code snippet is placed at the top of all protected JSP pages, such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp:

Listing 4

// ... response.setHeader( "Cache-Control"、 "no-cache"); //キャッシュに、オリジンサーバーからページの新しいコピーを取得するように強制しますresponse.setHeader( "Cache-Control"、 "no-store"); //どのような状況でもページを保存しないようにキャッシュに指示しますresponse.setDateHeader( "Expires"、0); //プロキシキャッシュにページを「古い」ものとして表示させますresponse.setHeader( "Pragma"、 "no-cache"); // HTTP1.0の下位互換性StringuserName =(String)session.getAttribute( "User"); if(null == userName){request.setAttribute( "Error"、 "セッションが終了しました。ログインしてください。"); RequestDispatcher rd = request.getRequestDispatcher( "login.jsp"); rd.forward(リクエスト、レスポンス); } //..。