12. MVC Web 应用程序 [person] – 版本 7
12.1. 简介
在此版本中,我们假设可能存在禁用了以下功能的客户端浏览器:
- 服务器发送 Cookie
- 执行显示的 HTML 页面中嵌入的 JavaScript 代码
尽管如此,我们仍希望此类浏览器能够使用我们的应用程序。第 2 点将我们带回应用程序的第 2 版,因为 JavaScript 是从第 3 版开始引入的。第 2 版是在不使用 JavaScript 的情况下运行应用程序的,因此第 2 点已得到解决。
问题1的处理难度因情况而异。我们的应用程序第6版在没有Cookie的情况下也能正常运行。通过合并第2版和第6版,我们便能达到预期效果。我们将增加一项额外限制:应用程序必须管理会话。这并非毫无意义的限制。在需要用户身份验证的应用程序中,服务器必须存储用户的用户名和密码,以避免用户在请求每个页面时都必须重新验证。
到目前为止,我们在客户端/服务器交互过程中使用了三种解决方案来存储信息:
- 会话
- Cookie
- 隐藏字段。
方案 2 可以排除,因为客户端浏览器可能已禁用 Cookie。
方案 3 是第 6 版中的方案,我们之前已经探讨过。出于安全原因,该方案不可行。如果登录名/密码对嵌入到发送给浏览器的每一页中,这意味着它们会在每次客户端与服务器之间的交互中通过网络传输。这对应用程序的安全性不利。 因此,我们可以考虑使用 HTTPS 协议,该协议可对客户端与服务器之间的通信进行加密。然而,若将 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 发回。
服务器对这一第二次请求作出了如下响应:
它找到了请求的页面并正在发送。请注意,它不再发送会话令牌。会话令牌通常的工作原理是:服务器以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 行:浏览器的首次请求。它未发送会话 Cookie。
- 第 11–17 行:服务器的响应,指示浏览器重定向到另一个 URL。它发送了一个会话 Cookie。第 13 行:<c:redirect> 标签已将令牌包含在第 14 行的重定向 URL 中。
- 第 19–27 行:浏览器的第二次请求。由于其 Cookie 功能已禁用,因此未回传服务器刚刚发送的会话 Cookie。
- 第 29–33 行:服务器的响应。 我们可以看到,尽管浏览器未发送会话 Cookie,但服务器并未如预期那样启动新会话。这一点从服务器未像第 13 行那样发送 [Set-Cookie] HTTP 头中可以明显看出。这意味着它延续了之前的会话。它之所以能检索到该会话,是因为第 19 行中浏览器请求的 URL 中包含了会话令牌。
请注意,服务器通过两种可能的方式检索客户端发送的会话令牌来跟踪会话:
- 通过客户端发送的 [Set-Cookie] HTTP 头
- 客户端请求的 URL 中
现在,使用同一浏览器,让我们像启用 Cookie 时那样,通过手动输入 URL [http://localhost:8080/personne4] 再次进行请求。随后我们将看到以下页面:

该结果与启用 Cookie 时所见不同:会话令牌出现在浏览器显示的 URL 中。让我们在不分析实际发生的 HTTP 交互的情况下解释这一结果:
[启用 Cookie]
- 在第二次请求 URL [http://localhost:8080/personne4] 时,客户端浏览器回传了它在第一次请求该 URL 时从服务器接收到的会话 Cookie。因此,<c:redirect> 标签没有将会话令牌包含在重定向地址中。
[已禁用Cookie]
- 在第二次请求 URL [http://localhost:8080/personne4] 时,由于客户端浏览器已禁用 Cookie,因此不会发送在第一次请求该 URL 时从服务器接收的会话 Cookie。因此,<c:redirect> 标签会在重定向 URL 中包含会话令牌。这就是它出现在上图截图中的原因。
<c:redirect> 和 <c:url> 标签允许您在 URL 中包含会话令牌。这就是本文提出的解决方案。
12.2. Eclipse 项目
要为 Web 应用程序 [/personne7] 创建 Eclipse 项目 [mvc-personne-07],请按照第 6.2 节所述的步骤复制项目 [mvc-personne-06]。
![]() | ![]() |
12.3. 配置 [personne7] Web 应用程序
/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. 视图代码
视图 [表单、响应、错误] 恢复到了第 2 版时的状态,即不包含 JavaScript。不过,它们保留了最新版本中的 JSTL 标签。
12.4.1. [表单]视图

与 JavaScript 代码相关的按钮已被移除。
[form.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> 标签编写,以便在客户端是不会发送 [Cookie] HTTP 头信息的浏览器时,也能包含会话令牌。
- 表单包含两个 [submit] 按钮:[Submit](第 28 行)和 [Clear](第 30 行)。这两个按钮的名称相同:button。当触发 POST 请求时,浏览器将发送以下参数:
- 若由 [Submit] 按钮触发 POST 请求,则发送参数:button=Submit
- 若由 [Clear] 按钮触发 POST 请求,则发送参数:button=Clear
该参数将帮助我们确定应采取的具体操作,因为 URL [/do/validationFormulaire] 现在对应于两个不同的操作。
12.4.2. [response] 视图

[response.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> 标签编写,以便在客户端是不会发送 [Cookie] HTTP 头信息的浏览器时,也能包含会话令牌。
12.4.3. [errors] 视图

[errors.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> 标签编写,以便在客户端是不会发送 [Cookie] HTTP 头信息的浏览器时,也能包含会话令牌。
欢迎读者采用与之前版本相同的方法测试这些新视图。
12.5. [ServletPersonne] 控制器
[/personne7] Web 应用程序的 [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] 操作由点击 [form] 视图中的 [Envoyer] 或 [Effacer] 按钮触发的 POST 请求引发。doValidationFormulaire 方法通过两种不同的方法处理这两种情况。
- 第 90–103 行:[doEnvoyer] 方法对应于上一版本中的 [doValidationFormulaire] 方法。输入的数据存储在会话中(第 96–98 行),而在上一版本中,数据被放置在请求中。
- 第 58–67 行:新的 [doEffacer] 方法必须显示一个空表单。我们可以调用 [doInit] 方法,该方法已执行此任务。在此,我们借此机会同时清除会话中的 [name, age] 元素,以便会话继续反映表单的最新状态。
- 第 50–55 行:请求显示 [form] 视图,且未对视图的模型进行任何明显的初始化。该模型实际上由会话中已有的 [name, age] 元素组成。无需采取进一步操作。
12.6. 测试
将 Eclipse 项目 [personne-mvc-07] 集成到 Tomcat 后,启动或重启 Tomcat,然后使用已禁用 Cookie 且已删除现有 Cookie 的浏览器请求 URL [http://localhost:8080/personne7]。将获得以下响应:

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

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

