12. Web 应用程序 MVC [personne] – 版本 7
12.1. 引言
在此版本中,我们假设可能存在某些客户端浏览器已禁用:
- 服务器发送的Cookie的回传
- 阻止在显示的 HTML 页面中执行嵌入的 JavaScript 代码
但我们仍希望此类浏览器能够使用我们的应用程序。第2点将我们带回了应用程序的第2版,因为JavaScript是从第3版开始使用的。第2版可以在不使用JavaScript的情况下运行应用程序,因此第2点已解决。
第1点可能难以处理,也可能并非如此。我们应用程序的第6版在不使用Cookie的情况下也能运行。通过合并第2版和第6版,我们得到了所需的结果。我们将增加一个额外的要求:应用程序必须支持会话管理。这并非毫无意义的要求。 在需要用户认证的应用程序中,服务器必须记住用户的用户名/密码组合,以避免用户在请求每个页面时都需重新认证。
到目前为止,我们已经使用了三种解决方案来在客户端/服务器交互过程中存储信息:
- 会话
- Cookie
- 隐藏字段
方案2可以排除,因为客户端浏览器可能已禁用Cookie功能。
方案3即之前探讨过的第6版方案。出于安全考虑,该方案不可行。如果登录名/密码对被封装在发送给浏览器的每一页中,这意味着它们会在每次客户端/服务器交互时通过网络传输。这对应用程序的安全性不利。 因此,可以考虑使用HTTPS协议来加密客户端与服务器之间的通信。但如果对应用程序的每一页都使用该协议,将会增加服务器的负载。
我们可能希望排除方案 1,因为它同样基于 Cookie。在首次客户端/服务器交互时,服务器会向客户端发送一个会话令牌,客户端将在每次新请求时将其发回给服务器。借助该令牌,服务器能够识别其客户端,并为其分配在先前交互中存储的信息。 会话令牌由服务器封装在 Cookie 中发送。 未禁用Cookie的浏览器可在后续请求中将该Cookie发回给服务器。若已禁用Cookie,则有另一种解决方案:可在请求的URL中包含会话令牌。现在我们重新研究第4版中的文件[index.jsp],即可看到这一情况:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main"/>
需要指出的是,上文第5行将客户端重定向至URL [/personne4/main?jsessionid=XX],其中 XX 即为会话令牌,如下图所示——该截图是在请求 URL [http://localhost:8080/personne4] 后获取的:

让我们更深入地探讨一下 <c:redirect> 标签在会话令牌方面的运作机制。假设使用的是允许 Cookie 的浏览器。下面我们将 Firefox 浏览器进行配置:

在 [1] 中,我们启用 Cookie;在 [2] 中,我们清除现有 Cookie 以建立已知环境。随后我们请求 URL [http://localhost:8080/personne4]。我们得到以下响应:

客户端的初始请求 HTTP 如下:
需注意的是,客户端并未发送会话cookie。服务器返回的响应HTTP如下:
- 第 1 行:服务器要求客户端重定向
- 第 3 行:服务器发送了一个与 [JSESSIONID] 属性关联的会话令牌
- 第 4 行:重定向 URL 包含会话令牌。由于客户端未发送会话 Cookie,因此由 <c:redirect> 标签将其放置于此。
被要求重定向的浏览器随后发出了以下请求:
- 第 1 行:它请求包含会话令牌的重定向 URL。这就是为什么在屏幕截图中,浏览器显示了该 URL。
- 第10行:浏览器将服务器在先前交互中发送的会话令牌发回。这是当客户端浏览器允许Cookie时,Cookie的正常工作方式。如果不允许,收到的Cookie将不会被发回。
服务器对该第二次请求作出了如下响应:
服务器已找到所请求的页面并将其发送。请注意,它不再发送会话令牌。这是会话令牌的正常工作原理:服务器仅以 Cookie 形式向浏览器发送一次,随后浏览器会在每次请求时将其发回以供识别。
现在,使用同一浏览器,通过手动输入 URL [http://localhost:8080/personne4] 再次请求该页面。此时将获得以下页面:

可以看到,浏览器显示的 URL 中不再包含会话令牌。让我们看看第一次客户端/服务器交互:
浏览器发出了以下请求:
这与上次的请求完全相同,但有一个区别:第10行,浏览器返回了它在首次交互中收到的会话令牌。再次强调,如果浏览器的Cookie功能处于启用状态,这是正常的行为。
服务器发送了以下响应:
它要求客户端进行重定向。由于已收到客户端的会话令牌,因此会继续使用该会话,而不会发送新的会话令牌。 出于同样的原因,<c:redirect> 标签不会将该会话令牌包含在重定向 URL 中。这就是为什么上图所示的 URL 中没有会话令牌。
由此可得出以下规则:只有当客户端未发送 HTTP 标头时,<c:redirect> 标签才会将会话令牌包含在重定向 URL 中:
这一规则同样适用于我们稍后将要遇到的 <c:url> 标签。
如果浏览器禁用了 Cookie 会发生什么?让我们试一试。首先,我们重置浏览器:

在 [1] 中,我们禁用 Cookie;在 [2] 中,我们清除已存在的 Cookie,以便从已知状态开始测试。随后我们请求 URL [http://localhost:8080/personne4]。我们得到以下响应:

我们得到了与之前相同的结果。然而,HTTP的交互内容却并不完全相同:
- 第1-9行:浏览器的第1次请求。它未发送会话cookie。
- 第11-17行:服务器的响应,要求浏览器重定向到另一个URL。该响应发送了一个会话Cookie第13行:<c:redirect>标签将令牌包含在重定向URL中(第14行)。
- 第19-27行:浏览器的第2次请求。由于其Cookie功能已被禁用,因此未返回服务器刚刚发送的会话Cookie。
- 第29-33行:服务器的响应。可以看出,尽管浏览器未发送会话cookie,但服务器并未如预期那样重新启动一个新会话。 这体现在服务器并未像第13行那样发送HTTP [Set-Cookie]这个头部。这意味着它延续了之前的会话。它能够通过第19行浏览器请求的URL中包含的会话令牌来识别该会话。
需要注意的是,服务器通过获取客户端发送的会话令牌来跟踪会话,主要有两种方式:
- 在客户端发送的 HTTP [Set-Cookie] 请求头中
- 客户端请求的 URL 中
现在,使用同一浏览器,像之前允许Cookie时那样,手动输入URL [http://localhost:8080/personne4] 再次进行请求。此时将显示以下页面:

该结果与允许Cookie时不同:会话令牌出现在浏览器显示的URL中。我们无需分析发生的HTTP数据交换,直接解释这一结果:
[cookies autorisés]
- 在第二次请求 URL [http://localhost:8080/personne4] 时,客户端浏览器回传了其在首次请求该 URL 时从服务器接收的会话 Cookie。因此,<c:redirect> 标签并未将会话令牌包含在重定向地址中。
[cookies inhibés]
- 在对 URL [http://localhost:8080/personne4] 的第二次请求中,由于客户端浏览器已禁用 Cookie,因此未发送其在对该 URL 的首次请求中从服务器接收的会话 Cookie。 因此,<c:redirect> 标签会在重定向地址中包含会话令牌。这就是为什么我们在上面的屏幕截图中看到了它。
<c:redirect> 和 <c:url> 标签允许将会话令牌包含在 URL 中。这就是本文提出的解决方案。
12.2. Eclipse 项目
要创建 Web 应用程序 [/personne7] 的 Eclipse 项目 [mvc-personne-07],需按照第 6.2 节所述步骤复制项目 [mvc-personne-06]。
![]() | ![]() |
12.3. Web 应用程序 [personne7] 的配置
应用程序 /personne7 的文件 web.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
...
<display-name>mvc-personne-07</display-name>
...
该文件与上一版本完全相同,仅第 3 行有所变化,其中 Web 应用程序的显示名称已更改为 [mvc-personne-07]。主页 [index.jsp] 保持不变。
...
<c:redirect url="/do/formulaire"/>
12.4. 视图代码
视图 [formulaire, réponse, erreurs] 恢复为第 2 版中的状态,即 c.a.d,不含 JavaScript。但它们保留了最新版本中的 JSTL 标签。
12.4.1. 视图 [formulaire]

已移除与 JavaScript 代码相关的按钮。
[formulaire.jsp]:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>Personne - formulaire</title>
</head>
<body>
<center>
<h2>Personne - formulaire</h2>
<hr>
<form name="frmPersonne" action="<c:url value="validationFormulaire"/>" method="post">
<table>
<tr>
<td>Nom</td>
<td><input name="txtNom" value="${nom}" type="text" size="20"></td>
</tr>
<tr>
<td>Age</td>
<td><input name="txtAge" value="${age}" type="text" size="3"></td>
</tr>
<tr>
</table>
<table>
<tr>
<td><input type="submit" name="bouton" value="Envoyer"></td>
<td><input type="reset" value="Rétablir"></td>
<td><input type="submit" name="bouton" value="Effacer"></td>
</tr>
</table>
</form>
</center>
</body>
</html>
- 第 14 行:POST 的目标 URL 使用 <c:url> 标签编写,以便在客户端是不会发送 HTTP [Cookie] 标头的浏览器时,会话令牌仍能包含其中。
- 表单中有两个类型为 [submit] 的按钮:[Envoyer](第 28 行)和 [Effacer](第 30 行)。这两个按钮名称相同:bouton。 当点击 POST 时,浏览器将发送参数:
- button=Submit(若 POST 请求由 [Submit] 按钮触发)
- 如果 POST 请求是由 [清除] 按钮触发的,则发送:button=清除
正是这个参数帮助我们确定了应执行的确切操作,URL [/do/validationFormulaire] 现在对应两项不同的操作。
12.4.2. 视图 [réponse]

[réponse.jsp]:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>Personne</title>
</head>
<body>
<h2>Personne - réponse</h2>
<hr>
<table>
<tr>
<td>Nom</td>
<td>${nom}</td>
</tr>
<tr>
<td>Age</td>
<td>${age}</td>
</tr>
</table>
<br>
<a href="<c:url value="retourFormulaire"/>">${lienRetourFormulaire}</a>
</body>
</html>
- 第 24 行:HREF 的目标 URL 使用 <c:url> 标签进行编写,以便在客户端是未发送 HTTP [Cookie] 标头的浏览器时,其中包含会话令牌。
12.4.3. 视图 [erreurs]

[erreurs.jsp]:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>Personne</title>
</head>
<body>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li>${erreur}</li>
</c:forEach>
</ul>
<br>
<a href="<c:url value="retourFormulaire"/>">${lienRetourFormulaire}</a>
</body>
</html>
- 第 18 行:HREF 的目标 URL 使用 <c:url> 标签编写,以便在客户端是不会发送 HTTP [Cookie] 标头的浏览器时,其中包含会话令牌。
建议读者按照之前版本中的原则测试这些新视图。
12.5. 控制器 [ServletPersonne]
Web 应用程序 [/personne7] 的控制器 [ServletPersonne] 如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | |
- 第 35 行:操作 [/retourFormulaire] 由 GET 执行,而非像前一版本那样由 POST 执行。
- 第 70-87 行:操作 [/validationFormulaire] 由 POST 触发,该操作由点击[formulaire]视图中的[Envoyer]或[Effacer]按钮之一所触发的POST方法。[doValidationFormulaire]方法通过两个不同的方法分别处理这两种情况。
- 第 90-103 行:方法 [doEnvoyer] 对应于上一版本中的方法 [doValidationFormulaire]。输入的数据被放入会话中(第 96-98 行),而在上一版本中,这些数据被放入查询中。
- 第 58-67 行:新方法 [doEffacer] 需显示一个空表单。可调用已实现此功能的方法 [doInit]。 在此,我们顺便将 [nom, age] 的元素从会话中清除,以确保会话能持续反映表单的最新状态。
- 第 50-55 行:请求显示视图 [formulaire],但未显式初始化该视图的模型。该模型实际上由会话中已存在的 [nom, age] 元素构成。无需进行其他操作。
12.6. 测试
将 Eclipse 项目 [personne-mvc-07] 集成到 Tomcat 后,启动或重启 Tomcat,然后使用已禁用 Cookie 并清除了现有 Cookie 的浏览器访问 URL [http://localhost:8080/personne7]。将获得以下响应:

浏览器接收到的源代码如下:
第1行,会话令牌位于POST的目标URL中。
填写表格并提交:

浏览器收到的源代码如下:
第3行,会话令牌位于链接的目标URL中。

