在客户端存储会话状态:设计详细信息

定制标记库在请求范围内查找实现 java.io.Serializable 的 Java 对象。Java 对象已使用 Java 序列化 API 进行了序列化并编码到 base64 代码中。我们选择使用 base64 编码,因为它是 HTTP 友好的、易于理解的编码。此策略不只局限于 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 显示了映射到前缀 cs 的定制标记库 "client-state",它负责将请求范围内的隐藏表单变量和 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

上面的表单已映射到 URI word 中,就像客户端状态标记在 action 属性中指定 word 一样。URI word 被映射到一个调用实用程序类的 Servlet 中,该类会查看参数 beanName 的表单值,即 sampleBean。名称属性为 sampleBean 的隐藏表单字段的值包含了 base64 编码的序列化内容。如果将客户端状态标记 secure 属性设置为 true,则还会对内容进行加密。

此设计具有以下局限性:

在客户端上存储状态有许多优点。使用定制标记策略对数据进行编码以及使用 Servlet 对状态进行解码,是提供状态管理和利用客户端存储状态的一个可重用的好方法。在客户端上存储状态会减轻服务器的负担,从而允许服务器支持更多的客户端。

确保客户端状态的安全

确保客户端状态安全的方法是对其进行加密并为它附加消息验证代码 (MAC)。代码示例 3 说明了如何为隐藏表单字段中存储的数据进行加密:
 // generate a key that can be used for encryption from the supplied password
byte[] rawKey = convertPasswordToKey(password);
// choose block encryption algorithm using a utility method
Cipher cipher = getBlockCipherForEncryption(rawKey);
// encrypt the plaintext
byte[] encdata = cipher.doFinal(plaindata);
// choose mac algorithm
Mac mac = getMac(rawKey);
// generate MAC for the initialization vector of the cipher
byte[] iv = cipher.getIV();
mac.update(iv);
// generate MAC for the encrypted data
mac.update(encdata);
// generate MAC
byte[] macBytes = mac.doFinal();
// concat byte arrays for MAC, IV, and encrypted data
// Note that the order is important here. MAC and IV are
// of fixed length and need to appear before the encrypted data
// for easy extraction while decrypting.
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 = // data obtained from the hidden form field
byte[] securedata = Base64.decode(hiddenfield);
// Extract MAC
byte[] macBytes = new byte[MAC_LENGTH];
System.arraycopy(securedata, 0, macBytes, 0, macBytes.length);
// Extract initialization vector used for encryption
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(securedata, macBytes.length, iv, 0, iv.length);
// Extract encrypted data
byte[] encdata = new byte[securedata.length - macBytes.length - iv.length];
System.arraycopy(securedata, macBytes.length + iv.length, encdata, 0, encdata.length);
// verify MAC by regenerating it and comparing it with the received value
byte[] rawKey = convertPasswordToKey(password);
Mac mac = getMac(rawKey);
mac.update(iv);
mac.update(encdata);
byte[] macBytesCalculated = mac.doFinal();
if (Arrays.equals(macBytes, macBytesCalculated)) {
// decrypt data only if the MAC was valid
Cipher cipher = getBlockCipherForDecryption(rawKey, iv);
byte[] plaindata = cipher.doFinal(encdata);
return plaindata;
}

代码示例 4:对从 POST 中隐藏表单字段接收的数据进行解密

下面的序列图 2 显示了如何在服务器端提取客户端状态。更新数据并对其重新加密,再发送回客户端。

序列图 2:对存储在客户端上的状态进行解密

index.jsp 中的客户端状态标记 action 属性被映射到 URI word 中,有关更多详细信息,请参见代码示例 2。URI word 被映射到 Servlet WordServlet,后者会调用实用程序类 ClientStateDeserializer,对包含 sampleBean 的隐藏表单字段的值进行解码和解密。重新构建参数 sampleBean 的值,方法是首先将 base64 编码的内容解码为字节数组,然后通过调用 ByteArrayGuard 中的解密方法对内容进行解密。重新构建的 SampleBean 对象放在名为 sampleBean 的请求范围属性中。然后 WordServlet 更新 Bean 的状态,并将请求转发回 index.jsp 页。index.jsp 页包括一个映射到 ClientStateTag 的客户端状态标记,该标记使用 ByteArrayGuard 对数据进行加密,并使用 base64 进行编码,然后将该数据作为客户端的隐藏表单字段返回给页面。

加密和解密例程都要求使用只有服务器知道并使用的口令。在此解决方案中,我们为每个 Web 用户使用不同的口令。不应该基于任何 Web 用户输入而生成此口令,因为这可能会影响其安全性。由于典型的 Web 应用程序允许用户进行自动注册,因此 Web 站点管理员来选择口令是不切实际的。因此,我们使用可配置的口令生成策略自动生成口令。该解决方案附带了一个 SessionPasswordStrategy,它可以随机生成口令并将其存储在 HttpSession 中。

此策略的缺点是客户端状态会在会话结束的同时丢失。通过实现在数据库中存储口令的替代策略,可以解决此问题。

参考资料

有关本主题的详细信息,请参阅以下资料:

© Sun Microsystems 2005。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。