Skip to content

12. MVC Web 应用程序 [person] – 版本 7

12.1. 简介

在此版本中,我们假设可能存在禁用了以下功能的客户端浏览器:

  1. 服务器发送 Cookie
  2. 执行显示的 HTML 页面中嵌入的 JavaScript 代码

尽管如此,我们仍希望此类浏览器能够使用我们的应用程序。第 2 点将我们带回应用程序的第 2 版,因为 JavaScript 是从第 3 版开始引入的。第 2 版是在不使用 JavaScript 的情况下运行应用程序的,因此第 2 点已得到解决。

问题1的处理难度因情况而异。我们的应用程序第6版在没有Cookie的情况下也能正常运行。通过合并第2版和第6版,我们便能达到预期效果。我们将增加一项额外限制:应用程序必须管理会话。这并非毫无意义的限制。在需要用户身份验证的应用程序中,服务器必须存储用户的用户名和密码,以避免用户在请求每个页面时都必须重新验证。

到目前为止,我们在客户端/服务器交互过程中使用了三种解决方案来存储信息:

  1. 会话
  2. Cookie
  3. 隐藏字段。

方案 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] 后获取的:

Image

让我们仔细看看 <c:redirect> 标签在会话令牌方面的具体工作原理。我们使用支持 Cookie 的浏览器进行演示。下面,我们将配置 Firefox 浏览器:

Image

在 [1] 中,我们启用 Cookie;在 [2] 中,我们删除所有现有 Cookie 以从已知状态开始。随后,我们请求 URL [http://localhost:8080/personne4]。我们收到以下响应:

Image

客户端的初始 HTTP 请求如下:

1
2
3
4
5
6
7
8
9
GET /personne4/ HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

请注意,客户端不会发送会话 Cookie。服务器发送的 HTTP 响应如下:

1
2
3
4
5
6
7
HTTP/1.x 302 Déplacé Temporairement
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=1ACA010A6BA28FB9E30A1D3184F574BC; Path=/personne4
Location: http://localhost:8080/personne4/main;jsessionid=1ACA010A6BA28FB9E30A1D3184F574BC
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 0
Date: Tue, 23 May 2006 09:10:05 GMT
  • 第 1 行:服务器要求客户端重定向
  • 第 3 行:服务器发送了一个与 [JSESSIONID] 属性相关联的会话令牌
  • 第 4 行:重定向 URL 包含会话令牌。由于客户端未发送会话 Cookie,因此由 <c:redirect> 标签将其放置于此。

被要求重定向的浏览器随后发出了以下请求:

GET /personne4/main;jsessionid=1ACA010A6BA28FB9E30A1D3184F574BC HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: JSESSIONID=1ACA010A6BA28FB9E30A1D3184F574BC
  • 第 1 行:它请求重定向 URL,其中包含会话令牌。这就是为什么浏览器在截图中显示此 URL 的原因。
  • 第 10 行:浏览器将服务器在上一次交互中发送给它的会话令牌发回。当客户端浏览器启用了 Cookie 功能时,Cookie 通常就是这样工作的。如果未启用 Cookie,则不会将接收到的 Cookie 发回。

服务器对这一第二次请求作出了如下响应:

1
2
3
4
5
HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 2376
Date: Tue, 23 May 2006 09:10:05 GMT

它找到了请求的页面并正在发送。请注意,它不再发送会话令牌。会话令牌通常的工作原理是:服务器以cookie的形式将其发送给浏览器一次,然后浏览器在每次请求时将其发回以供识别。

现在,使用同一浏览器,让我们通过手动输入 URL [http://localhost:8080/personne4] 再次进行请求。随后我们将看到以下页面:

Image

我们可以看到,浏览器显示的 URL 中不再包含会话令牌。让我们看看第一次客户端/服务器交互:

浏览器发出了以下请求:

GET /personne4 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: JSESSIONID=1ACA010A6BA28FB9E30A1D3184F574BC

这与上一个请求完全相同,只有一个区别:在第 10 行,浏览器回传了它在最初一次交互中收到的会话令牌。同样,如果浏览器的 Cookie 功能已启用,这是正常的行为。

服务器返回了以下响应:

1
2
3
4
5
HTTP/1.x 302 Déplacé Temporairement
Server: Apache-Coyote/1.1
Location: http://localhost:8080/personne4/
Transfer-Encoding: chunked
Date: Tue, 23 May 2006 09:24:39 GMT

它指示客户端进行重定向。由于它从客户端接收到了会话令牌,因此会继续当前会话,而不会发送新的会话令牌。出于同样的原因,<c:redirect> 标签不会将会话令牌包含在重定向 URL 中。这就是为什么上图截图中显示的 URL 不包含会话令牌。

从以上内容中得出的关键要点是以下规则:只有当客户端未发送 HTTP 头时,<c:redirect> 标签才会将会话令牌包含在重定向 URL 中:

Cookie: JSESSIONID=1ACA010A6BA28FB9E30A1D3184F574BC

这条规则也适用于 <c:url> 标签,我们稍后会遇到它。

如果浏览器禁用了 Cookie,会发生什么情况?让我们试一试。首先,我们重置浏览器:

Image

在 [1] 中,我们禁用 Cookie;在 [2] 中,我们删除所有现有 Cookie,以便从已知状态开始。然后,我们请求 URL [http://localhost:8080/personne4]。我们得到以下响应:

Image

结果与之前相同。不过,HTTP 交互过程并非完全一致:

GET /personne4/ HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

HTTP/1.x 302 Déplacé Temporairement
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=911B8156E0A9D32C2D256020C898E05C; Path=/personne4
Location: http://localhost:8080/personne4/main;jsessionid=911B8156E0A9D32C2D256020C898E05C
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 0
Date: Tue, 23 May 2006 09:39:55 GMT

GET /personne4/main;jsessionid=911B8156E0A9D32C2D256020C898E05C HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: fr-fr,fr;q=0.8,en;q=0.6,en-us;q=0.4,de;q=0.2
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 2376
Date: Tue, 23 May 2006 09:39:55 GMT
  • 第 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] 再次进行请求。随后我们将看到以下页面:

Image

该结果与启用 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. [表单]视图

Image

与 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] 视图

Image

[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] 视图

Image

[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] 控制器如下:

package istia.st.servlets.personne;

...

@SuppressWarnings("serial")
public class ServletPersonne extends HttpServlet {
    // instance parameters
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres={"urlFormulaire","urlReponse","lienRetourFormulaire"};
    private Map params=new HashMap<String,String>();

    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
...
    }

    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

...
        // retrieve the request sending method
        String méthode=request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action=request.getPathInfo();
...
        if(méthode.equals("post") && action.equals("/validationFormulaire")){
            // validation of input form
            doValidationFormulaire(request,response);
            return;
        }
        if(méthode.equals("get") && action.equals("/retourFormulaire")){
            // back to input form
            doRetourFormulaire(request,response);
            return;
        }
        // other cases
        doInit(request,response);
    }

    // empty form display
    void doInit(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
...
    }

    // display pre-filled form
    void doRetourFormulaire(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        // the form is displayed
        getServletContext().getRequestDispatcher((String)params.get("urlFormulaire")).forward(
                request, response);
        return;
    }

    // empty form display
    void doEffacer(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        // prepare the form template
        HttpSession session = request.getSession(true);        
        session.setAttribute("nom", "");
        session.setAttribute("age", "");
        // the form is displayed
        getServletContext().getRequestDispatcher((String)params.get("urlFormulaire")).forward(
                request, response);
        return;
    }

    // form validation
    void doValidationFormulaire(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException{
        // we retrieve the button that caused the POST
        String bouton = request.getParameter("bouton").toLowerCase();
        // treatment according to the button that caused the POST
        if(bouton==null){
            doInit(request,response);
            return;
        }
        if("envoyer".equals(bouton)){
            doEnvoyer(request,response);
            return;
        }
        if("effacer".equals(bouton)){
            doEffacer(request,response);
            return;
        }
    }

    // form validation
    void doEnvoyer(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException{
        // parameters are retrieved
        String nom = request.getParameter("txtNom");
        String age = request.getParameter("txtAge");
        // stored in the session
        HttpSession session = request.getSession(true);        
        session.setAttribute("nom", nom);
        session.setAttribute("age", age);
        // the link back to the form is set in the view template [response, errors]
        request.setAttribute("lienRetourFormulaire", (String)params.get("lienRetourFormulaire"));    
        // parameter verification
        ArrayList<String> erreursAppel = new ArrayList<String>();
    ...
        // errors in the parameters?
        if (erreursAppel.size() != 0) {
            // send error page
            request.setAttribute("erreurs", erreursAppel);
            getServletContext().getRequestDispatcher(urlErreurs).forward(
                    request, response);
            return;
        }
        // parameters are correct - send response page
        getServletContext().getRequestDispatcher((String)params.get("urlReponse")).forward(request,
                response);
        return;
    }

    // post
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
...
    }
}
  • 第 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]。将获得以下响应:

Image

浏览器收到的源代码如下:

1
2
3
<form name="frmPersonne" action="validationFormulaire;jsessionid=9D4CC83FEFB51AE78B1FD71EC66F9EF3" method="post">
...
</form>

第 1 行:会话令牌位于 POST 目标 URL 中。

让我们填写表单并提交:

Image

浏览器收到的源代码如下:

1
2
3
4
...
    <br>
    <a href="retourFormulaire;jsessionid=9D4CC83FEFB51AE78B1FD71EC66F9EF3">Retour au formulaire</a>
  </body>

第 3 行:会话令牌位于链接的目标 URL 中。