カスタムタブライブラリは、java.io.Serializable
を実装している Java オブジェクトを要求スコープで探します。そうした Java オブジェクトは Java シリアライゼーション API を使用してシリアライズされ、base64 エンコーディングで符号化されます。この例では、HTTP フレンドリであり、よく理解されているため、base64 エンコーディングを採用しています。この方法は、base64 エンコーディングに限定されるわけではなく、要求スコープ内の Java オブジェクトだけをエンコーディングするわけでもありません。クライアント側状態のセキュリティー保護という対処法では、不正なクライアントがこの状態を操作するのを防ぐ方法を説明しています。
<cs:client-state action="word" beanName="sampleBean" secure="true">
<table>
<c:forEach var="word" varStatus="status" begin="0" items="${sampleBean.words}">
<tr>
<td>
<input type="checkbox" name="word_${status.index}" value="${word}">
${word}
</input>
</td>
</tr>
</c:forEach>
<tr>
<td>
New Word: <input type="text" name="newWord" size="25"/>
</td>
</tr>
<tr>
<td>
<input type="submit" value="Update List"/>
</td>
</tr>
</table>
</cs:client-state>
コード例 1: クライアント側状態のタグ化
コード例 1 は、カスタムライブラリ "client-state" の、接頭辞 cs
へのマッピングを示しています。このコードは、要求スコープ内の非表示のフォーム変数と URL パラメータを上記の base64 表現に変換する仕事をします。コード例 2 は、このタグによって生成される HTML です。
<form method="POST" action="word">
<input type="hidden" name="beanName" value="sampleBean" />
<input type="hidden" name="sampleBean" value="rO0ABXNyAC5jb20uc3VuLmoyZWUuYmx1ZXByaW50cy5jbGllbnRzdGF0ZS5TYW1wbGVCZWFuhCyFzCR588MCAAJMAAhuZXh0UGFnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABXdvcmRzdAAVTGphdmEvdXRpbC9BcnJheUxpc3Q7eHBwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAJ3BAAAAAp0AARncmVndAAFaW5kZXJ4" />
<table>
<tr>
<td>
<input type="checkbox" name="word_0" value="greg">
greg
</input>
</td>
</tr>
<tr>
<td>
<input type="checkbox" name="word_1" value="inder">
inder
</input>
</td>
</tr>
<tr>
<td>
New Word: <input type="text" name="newWord" size="25"/>
</td>
</tr>
<tr>
<td>
<input type="submit" value="Update List"/>
</td>
</tr>
</table>
</form>
コード例 2: クライアント側状態タグで生成された HTML
上記のフォームは、client-state タグにより、action
属性の指定に従って URI の word
にマッピングされます。そして、この URI word
が、beanName
パラメータのフォーム値 (ここでは sampleBean
) を調べるユーティリティークラスを呼び出すサーブレットにマッピングされます。名前属性 sampleBean
を持つ非表示フォームフィールドの値には、base64 エンコーディングのシリアライズされた内容が含まれます。client-state タグの secure
属性が true
に設定されている場合、その内容の暗号化も行われます。
このデザインには、以下のような制限があります。
クライアントに状態を保存することには、多くの利点があります。カスタムタグを使用した対処法でデータをエンコーディングし、サーブレットを使用して状態をデコーディングすることは、状態管理機能を提供して、クライアントで状態を保存する再利用可能な優れた手段の 1 つです。クライアントで状態を保存することによってサーバーが負担がなくなり、サーバーがサポートするクライアントを増やすことができます。
メッセージ認証コード (MAC) を暗号化し、付けることによって、クライアント側状態をセキュリティー保護します。コード例 3 は、非表示フォームフィールドに保存されているデータに対する暗号化方法を示しています。// 提供されたパスワードから、暗号化に使用できるキーを生成
byte[] rawKey = convertPasswordToKey(password);
// ユーティリティーメソッドを使用して、ブロック暗号化アルゴリズムを選択
Cipher cipher = getBlockCipherForEncryption(rawKey);
// プレーンテキストを暗号化
byte[] encdata = cipher.doFinal(plaindata);
// MAC アルゴリズムを選択
Mac mac = getMac(rawKey);
// サイファの初期化ベクトルの MAC を選択
byte[] iv = cipher.getIV();
mac.update(iv);
// 暗号化したデータの MAC を生成
mac.update(encdata);
// MAC を生成
byte[] macBytes = mac.doFinal();
// MAC、IV、暗号化されたデータのバイト配列を連結
// ここでは、順序が重要であることに注意。
// MAC および IV は固定長であり、解読中の抽出を容易にするには、
// 暗号化されたデータの前にくる必要がある。
byte[] tmp = concatBytes(macBytes, iv);
byte[] securedata = concatBytes(tmp, encdata);
byte[] outputdata = Base64.encode(securedata);
コード例 3: 非表示フォームフィールドに保存するデータの暗号化
暗号化プロセスは、パスワードから生成されたキーを使用して、サイファ (暗号化アルゴリズム) を初期化します。この初期化中、サイファは初期化ベクトル (IV) を生成します。この初期化ベクトルは、暗号化アルゴリムの実行のたびに、同じ入力に対して異なる出力が生成されるようにするためにサイファが使用する乱数です。IV は秘密ではなく、暗号化された内容とともに保持されます。暗号化された内容と IV を不正操作できないようにするため、MAC および IV から MAC が生成されます。この MAC もまた、初期化ベクトルを使用して、MAC アルゴリムの実行のたびに、同じデータに対して異なる MAC が生成されるようにすることに注意してください。この出力 (コード例 3 の securedata
) は、MAC に続く IV, そして暗号化されたデータから構成されます。MAC および IV は固定長であるため、暗号化されたデータの前に保持され、このため、解読中に容易に抽出できます。
暗号化プロセスの出力は、base64 エンコーディングでテキストに変換されます。そしてこのテキストが、クライアントに送信される HTML の非表示フィールドに保存されます。シーケンス図 1 は、SampleBean のサーバー側状態がサーバー側での暗号化の方法とクライアントでの保存方法を示しています。
シーケンス図 1:クライアントに保存する状態の暗号化
クライアントからフォームが送信されると、非表示フィールドが抽出され、最初に、base64 エンコーディングをデコーディングすることによって、バイト配列に変換されます。MAC を調べてることによってテンパリングが行われていないことを確認してから、この内容が解読されます。コード例 4 は、この方法を示しています。
byte[] hiddenfield = // 非表示フォームフィールドから取得したデータ
byte[] securedata = Base64.decode(hiddenfield);
// MAC を抽出
byte[] macBytes = new byte[MAC_LENGTH];
System.arraycopy(securedata, 0, macBytes, 0, macBytes.length);
// 暗号化に使用されていた初期化ベクトルを抽出
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(securedata, macBytes.length, iv, 0, iv.length);
// 暗号化されたデータを抽出
byte[] encdata = new byte[securedata.length - macBytes.length - iv.length];
System.arraycopy(securedata, macBytes.length + iv.length, encdata, 0, encdata.length);
// MAC を再生成し、受信した値を比較することによって、MAC を検査
byte[] rawKey = convertPasswordToKey(password);
Mac mac = getMac(rawKey);
mac.update(iv);
mac.update(encdata);
byte[] macBytesCalculated = mac.doFinal();
if (Arrays.equals(macBytes, macBytesCalculated)) {
// MAC が有効な場合にのみデータを解読
Cipher cipher = getBlockCipherForDecryption(rawKey, iv);
byte[] plaindata = cipher.doFinal(encdata);
return plaindata;
}
コード例 4: POST で受信した非表示フィールドのデータの解読
次のシーケンス図 2 は、サーバー側でのクライアント側状態の抽出方法を示しています。データが更新され、再暗号化されて、クライアントに返送されます。
シーケンス図 2: クライアントに保存する状態の解読
index.jsp
内の client-state タグの action
属性は、URI の word
にマッピングされます (詳細はコード例 2 を参照)。この URI word
は、ユーティリティークラス ClientStateDeserializer
を呼び出して、sampleBean
を含む非表示フォームフィールドの値をデコーディングし、解読するサーブレット WordServlet
にマッピングされます。パラメタ sampleBean
の値は、base64 エンコーディングの内容をバイト配列にデコーディングしてから、ByteArrayGuard
内の解読メソッドを呼び出して解読することによって再構成されます。再構成された SampleBean オブジェクトは、sampleBean
という名前で要求スコープ属性に設定されます。WordServlet
は、この Bean の状態を交信し、index.jsp
ページに要求を返します。index.jsp
ページには、ByteArrayGuard
を使用してデータを暗号化し、base64 でエンコーディングして、クライアントの非表示フォームフィールドとして、そのページにデータを返す ClientStateTag
にマッピングされている client-state タグが含まれています。
暗号化および解読ルーチンでは、サーバーのみが認識、使用するパスワードを使用する必要があります。この対処法では、Web ユーザーごとにパスワードを変えています。安全性が損なわれる可能性があるため、このパスワードは、Web ユーザーからの入力に基づいて生成しないでください。一般的な Web アプリケーションはユーザーに対して自己登録を行えるようにしているため、パスワードの選択に Web サイトの管理者が関与するのは実際的ではありません。このため、この例では、変更可能なパスワード生成方法を使用して、パスワードを自動生成しています。この対処法には、ランダムにパスワードを生成し、それを HttpSession に保存する SessionPasswordStrategy が含まれています。
この方法の欠点は、セッションが期限切れになったときにクライアント側の状態が失われることです。この問題は、パスワードをデータベースに保存する別の方法を実装することによって対処できます。