本章要点

  • 了解用于会话跟踪的三种技术
  • 掌握 Session 的使用
  • 掌握基于Cookie 的会话跟踪
  • 掌握使用 URL 重写的机制来进行会话跟踪
  • 理清常见的关于Session 的错误认知
  • 了解 Session 的持久化

引言介绍

随着网络的发展,不少人都具有了网络购物的经验,也许读者正在看的这本书也是从网上书店购买的。让我们考虑一下在网上书店购书的场景,用户A登录到一家网上书店,选购了一本书《struts2 深入详解》,放入了购物车,然后接着浏览其他的书籍,那么对于web服务器来说,它需要记住购物车中已经有了一本书。此时用户B也登录到这家网上书店,并选购了两本书,放进了购物车,然后进入收银台确认订单,进行结算, 可是服务器要如何区分结算的购物车对应的是哪一个用户呢?读者可能会想到,Web服务器为每一个用户创建一个购物车,那么结算时,自然知道是哪一个用户购买的物品要进行结算。话虽如此,但是要注意的是,购物车的状态是在服务器端维护的,而客户端与服务器进行通信的协议—-HTTP协议, HTTP协议本身是基于请求/响应模式的、无状态的协议,也就是说,当客户端的请求到来,服务器端做出响应后,连接就被关闭了。即使HTTP1.1支持持续连接,但是当用户有一段时间没有提交请求(正在看图书的简介)连接也将被关闭,那么当下一个用户与服务器建立连接,发出请求的时候,服务器怎么知道在服务器端维护的购物车是属于这个用户的,还是其他用户的呢?
这样的问题在很多应用中都存在,例如,我要从网上银行给朋友汇一笔钱,输入用户名和密码,服务器验证通过后,发回操作页面,然后连接关闭。在我进行账户资金划账的时候,连接重新建立,但是服务器如何能保证正在转账的账户就是我的账户呢? 因为,此时可能还有很多人在进行转账的操作。
用户的活动行为发生在多个请求和响应中,作为Web服务器来说,必须能够采用一种机制来唯一地标识一个用户,同时记录该用户的状态,这是一个Web应用程序典型的需求。
为了实现上述需求,需要一下两种机制:
① 会话: 服务器应当能够标识出 来自单个客户的一系列请求(备注:一段时间内一组请求),并把这些请求组成一个单独的工作 “会话”。通过把特定请求与一个特定的工作会话相联系(备注:绑定关联),无论购物车应用程序或者在线银行应用程序就能够把一个用户与另一个用户区分开。
②状态:服务器应当能够记住前面请求的信息,以及对前一请求作出的 处理信息。也就是说,服务器应当处于 会话联系 状态(备注:会话联系是一种状态,对于单个客户来说就是从第一次访问建立连接到会话失效关闭连接处于会话联系状态)。对于购物车应用程序来说,可能的状态包括用户喜欢的项目类别,用户的配置信息和购物车本身。(备注:例如 人 在每个时间段(这个时间段可能很短,也可能很长)都会表现一种状态,吃饭状态、睡觉状态,购物状态、操作状态等。 eg:万物皆对象 , 所以购物车等也会有不同时期的状态)

5.1 用于会话跟踪的技术

Java Servlet API 使用 Session 来跟踪会话和管理会话内的状态。利用Session,服务器可以把一个客户的所有请求联系在一起,并记录客户的操作状态。当客户第一次连接到服务器的时候,服务器为其建立一个session,并分配给客户一个唯一的标识(Session ID),以后客户每次提交请求,都要将标识一起提交。服务器根据标识找出特定的Session,用这个session记录客户的状态。
这个过程就好像我们去超市购物存包的过程:一个顾客(相当于Web客户端)去超市购物,到存包处(相当于web服务器)存包,管理员将顾客的包放到一个柜子里(相当于建立一个Session),然后将一个号码牌交给顾客(相当于为顾客分配一个唯一的SessionID)。当顾客下一次到存包处的时候,需要将号码牌交给管理员,管理员根据号码牌找到对应的柜子,根据顾客的请求(HTTP Request)取出、添加、更换物品,然后将号码牌再次交给顾客。顾客每次到存包处的时候,都要提供号码牌,存包处的管理员对顾客的请求做出响应(HTTP Response)后,需要将号码牌再次交还给顾客。
从上面的过程中,我们可以看出,通过在每一个请求和响应中包含 Session ID,服务器可以将一个用户与另一个用户区分开。
在Servlet 规范中,描述了下列三种机制用于会话跟踪:

  1. SSL(secure Socket Layer, 安全套接字层)会话
  2. Cookies
  3. URL重写

下面介绍这三种机制。

5.1.1 SSL 会话

SSL (Secure Scoket Layer, 安全套接字层)是一种运行在TCP/IP 之上和 像HTTP这种应用层协议之下的加密技术。SSL 是在HTTPs 协议中使用的加密技术。SSL可以让采用SSL的服务器认证采用SSL的客户端,并且在客户端和服务器之间保持一种加密的连接。在建立了加密连接的过程中,客户端和服务器都可以产生名叫“会话密钥”的东西,它是一种用于加密和解密消息的对称密钥。基于HTTPs协议的服务器可以使用这个客户的对称密钥来建立会话。

5.1.2 Cookies

Cookies ,中文翻译为小甜饼,由Netscape公司发明,是最常用的跟踪用户会话的方法。Cookies 是一种由服务器发送给客户的片段信息,存储在客户浏览器的内存中或硬盘上,在客户随后对该服务器的请求中发回它。
Cookie 有三个规范,原始的Netscape 规范(版本0),RFC2109 ( HTTP 状态管理机制,版本1)和RFC2965(HTTP状态管理机制,版本1,是对RFC2109的替代)。在servlet2.5中的Cookie支持Netscape规范和RFC2109.为了确保最好的互操作性,在默认情况下,Java Servlet API 采用版本0 (依照 Netscape规范)创建Cookie。读者可以在 链接 上面找到RFC 2109 和 RFC 2965 文件,或者在搜索引擎上搜索这两个RFC 文件。Netscape规范位于下面的网站: 链接
Cookies 以键-值对的方式记录会话跟踪的内容,服务器利用响应报头 Set-Cookie来发送 Cookie信息。在RFC 2109 中定义的 Set-Cookie 响应报头的格式为:

  1. Set-Cookie:NAME=VALUE; Comment=value; Domain=value; Max-Age=value; Path=value; Secure;
  2. Version=1*DIGIT

NAME 是 Cookie 的名字,VALUE是它的值。NAME=VALUE 属性-值 对必须首先出现, 在此之后的属性-值对可以以任何顺序出现。在Servlet规范中,用于会话跟踪的Cookie的名字必须是JSESSIONID.

  1. Comment 属性是可选的,因为Cookies可能包含关于用户私有的信息,这个属性允许服务器说明这个cookie的使用,用户可以检查这个信息,然后决定是否加入或继续会话。
  2. Domain 属性是可选的,用于指定Cookie在哪一个域中有效,所指定的域必须以点号(.) 开始。
  3. Max-Age 属性是可选的,用于定义Cookie的生存时间,以秒为单位,如果超过了这个时间,客户端该丢弃这个Cookie, 如果指定的秒数为0,表示这个cookie应该立即被丢弃。
  4. Pat h属性是可选的,用于指定这个Cookie在哪一个URL子集下有效。
  5. Secure 属性可选的,它没有值,用于指示浏览器使用安全的方式与服务器交互。
  6. Version 属性是必须的,它的值是一个十进制的整数,标识Cookie 依照的状态管理规范的版本,对于RFC2109,Version 应用设为 1.
    1. Set-Cookie: uid=zhangsan; Domain=.sunxin.org; Max-Age=3600; Path=/bbs;
    2. Version=1
    上面这个响应报头发送了一个名为uid, 值为zhangsan的 Cookie,Cookie的生存时间为3600秒,在sunxin.org域的 /bbs 路径下有效。在3600秒之后,浏览器应该丢弃这个Cookie。
    当浏览器收到上面这个响应报头后,可以选择拒绝或者接受这个Cookie.如果浏览器接受了这个cookie, 当浏览器下一次发送请求给http://www.sunxin.org/bbs/路径下的资源时,同时也会发送下面的请求报头:
    1. Cookie: uid=zhangsan
    服务器从请求头中得到Cookie,然后通过标识取出在服务器中存储的zhangsan的状态信息(备注:),这样,通过为不同的用户发送不同的Cookie,就可以实现每个用户的会话跟踪。
    因为Cookie是在响应报头和请求报头中被传送的,不与传送的正文内容混淆在一起,所以Cookie的使用对于用户来说是透明的。然而也正是因为Cookie对用户是透明的,加上Cookie持久性高,可以长时间的追踪用户(Cookie 可以保存在用户机器的硬盘上)了解用户上网的习惯,而用户在网上的一举一动,就有可能成为某些网站或厂商赚钱的机会,这就造成了一些隐私权和安全性方面的问题。于是有些用户在使用浏览器时,会选择禁用Cookie,这样的话,Web服务器就无法利用Cookie来跟踪用户的会话了,要解决这个问题,就要用到下一节介绍的 URL 重写机制。

    5.1.3 URL 重写机制

    当客户端不接受Cookie的时候,可以使用URL重写的机制来跟踪用户的会话。URL重写就是在URL中附加 标识客户的SessionID, 当Servlet 容器 解释URL时,取出Session ID,根据Session ID 将请求与特定的Session关联。
    Session ID 被编码为URL字符串中的路径参数, 在 Servlet 规范中,这个参数的名字必须是 jsessionid, 下面是一个包含了编码后的路径信息的URL 的例子;
    1. http://www.sunxin.org/bbs/index.jsp;jsessionid=1234
    还可以在后面加上查询字符串,完整的URL 如下所示:
    1. http://www.sunxin.org/bbs/index.jsp;jsessionid=1234?name=zhangsan&age=18
    服务器将Session ID 作为URL的一部分发送给客户端,客户端在请求URL 中再传回来,这样,Web服务器就可以跟踪用户的会话了。
    要跟踪客户端的会话,就需要将所有发往客户端的URL进行编码,这可以通过调用HttpServlet 接口中的encodeURL()方法来实现,其中,encodeRedirectURL()方法主要在sendRedirect()方法调用之前使用,用于编码重定向的URL.

5.2 Java Servlet API 的会话跟踪

在 Java Servlet API 中,javax.servlet.http.HttpSession 接口封装了Session 的概念,Servlet 容器 提供了这个接口的实现。当请求一个会话的时候,Servlet容器就创建一个HttpSession对象,有了这个对象后,就可以利用这个对象(session)中保存客户的状态信息,例如,购物车。
Servlet 容器 为HttpSession对象分配一个唯一的Session ID ,将其作为Cookie(或者作为URL的一部分,利用URL 重写机制)发送给浏览器,浏览器在内存中保存这个Cookie,当客户再次发送HTTP 请求时,浏览器将Cookie随请求一起发送,Servlet 容器从请求对象中读取Session ID ,然后根据 Session ID 找到对应的HttpSession对象,从而得到客户的状态信息。整个过程如图5-1所示。
整个会话跟踪过程对于客户和开发人员都是透明的(因为由Servlet容器 来完成),开发人员所要做的就是得到HttpSession对象,然后调用这个对象 setAttribute() 或 getAttribute()方法来保存或读取客户的状态信息。

IMG20211222235012.jpg

5.2.1 HttpSession 接口

HttpSession 接口提供了下列方法:

  • public java.lang.Object getAttribute(java.lang.String name)
  • public java.util.Enumeration getAttributeNames()
  • public void removeAttribute( java.lang.String name)
  • public void setAttribute(java.lang.String name, java.lang.Object value)

上面四个方法用于在HttpSession对象中读取、移除和设置属性,利用这些方法,可以在Session 中维护客户的状态信息。
-> public long getCreationTime()
返回Session 创建的时间,这个时间是从1970年 1月 1日 00:00:00 GMT 以来的毫秒数。
-> public java.lang.String getId()
返回一个字符串,其中包含了分配给Session的唯一标识符。这个标识符是由Servlet容器分配的,与具体的实现关系。
-> public long getLastAccessedTime()
返回客户端最后一次发送与Session相关的请求的时间,这个时间是从1970年 1月 1日00:00:00 GMT 以来的毫秒数。这个方法可以用来确定客户端在两次请求之间的会话的非活动时间。
-> public int getMaxInactiveInterval()
返回以毫秒为单位的最大时间间隔。这个时间值是Servlet容器在客户的两个连续请求之间保持Session打开的最大时间间隔,超过这个时间间隔,Servlet容器将使Session 失效。
-> public void setMaxInactiveInterval(int interaval)
这个方法用于设置在Session 失效之前,客户端的两个连续请求之间的最大时间间隔。如果设置一个负值,表示Session永远不会失效。Web应用程序可以使用这个方法来设置Session的超时时间间隔。
-> public ServletContext getServletContext()
返回Session所属的ServletContext对象。
-> public void invalidate()
这个方法用于使会话失效。例如,用户在网上书店购买完图书后,可以选择退出登录,服务器端的Web应用程序可以调用该方法使Session会话失效,从而让用户不再与这个Session 关联。
-> public boolean isNew()
如果客户端还不知道这个Session或者客户端没有选择加入Session ,那么这个方法将返回true ,例如,服务器使用基于Cookie的Session,而客户端禁用了Cookie,那么对每一个请求,Session 都是新的。
要得到一个Session对象,可以调用HttpServletRequest 接口的getSession()方法;
如下所示:
-> public HttpSession getSession(Boolean create )
该方法返回与此次请求相关联的Session,如果没有给客户端分配 Session,而create 参数为true, 则创建一个新的 Session。如果 create 参数为 false,而此次请求没有一个有效的HttpSession, 则返回null。

5.2.2 Session 的生命周期

在这一节,我们将通过一个登陆程序来演示Session 的生命周期。刚开始,这个程序采用基于Cookie 的会话跟踪,当客户端禁用Cookie后, 采用URL重写的机制进行会话跟踪。实例的开发主要步骤。

step1:编写OutputSessionInfo类

OutputSessionInfo 是一个工具类,它有个静态的方法,该方法以表格的形式输出 Session 的相关信息。

  1. public class OutputSessionInfo {
  2. public static void printSessionInfo(PrintWriter out, HttpSession session){
  3. out.println("<table>");
  4. out.println("<tr>");
  5. out.println("<td>会话的状态:</td>");
  6. if (session.isNew()) {
  7. out.println("<td> 新的会话</td>");
  8. }else {
  9. out.println("<td> 旧的会话 </td>");
  10. }
  11. out.println("</tr>");
  12. out.println("<tr>");
  13. out.println("<td> 会话 ID:</td>");
  14. out.println("<td>" + session.getId() + "</td>");
  15. out.println("</tr>");
  16. out.println("<tr>");
  17. out.println("<td>创建时间:</td>");
  18. out.println("<td>" + new Date(session.getCreationTime()) + "</td>");
  19. out.println("</tr>");
  20. out.println("<tr>");
  21. out.println("<td>上次访问时间:</td>");
  22. out.println("<td>" + new Date(session.getLastAccessedTime()) + "</td>");
  23. out.println("</tr>");
  24. out.println("<tr>");
  25. out.println("<td>最大不活动时间间隔:</td>");
  26. out.println("<td>" + new Date(session.getMaxInactiveInterval()) + "</td>");
  27. out.println("</tr>");
  28. out.println("</table>");
  29. }
  30. }

step2: 编写LoginServlet 类

  1. public class LoginSerlvet extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp)
  4. throws ServletException, IOException {
  5. resp.setContentType("text/html;charset=gb2312");
  6. final HttpSession session = req.getSession();
  7. String user = (String) session.getAttribute("user");
  8. PrintWriter out = resp.getWriter();
  9. out.println("<html>");
  10. out.println("<meta http-equiv=\"Pragma\" content=\"no-cache\">");
  11. out.println("<head><title>登录页面</title></head>");
  12. out.println("<body>");
  13. OutputSessionInfo.printSessionInfo(out, session);
  14. out.println("<p>");
  15. out.println("<form method=post action=/itguigu/loginchk>");
  16. out.println("<table");
  17. out.println("<tr>");
  18. out.println("<td> 请输入用户名</td>");
  19. if (user == null) {
  20. out.println("<td><input type=text name=user></td>");
  21. }else{
  22. out.println("<td><input type=text name=user value="+user+"></td>");
  23. }
  24. out.println("</tr>");
  25. out.println("<tr>");
  26. out.println("<td>请输入密码</td>");
  27. out.println("<td><input type=password name=password></td>");
  28. out.println("</tr>");
  29. out.println("<tr>");
  30. out.println("<td><input type=reset value=重填></td>");
  31. out.println("<td><input type=submit value=登录></td>");
  32. out.println("</tr>");
  33. out.println("</table>");
  34. out.println("</form");
  35. out.println("</body>");
  36. out.println("</html>");
  37. out.close();
  38. }

第9行代码 调用请求对象的getSession()方法来得到和这个请求相联系的HttpSession对象,如果这个请求还没有一个Session,Servlet容器会创建一个。
第10行代码,调用HttpSession对象的 getAttribute(“user”)方法获取在Session中存储的名字为user的属性。将用户的登录信息保持在Session中,当用户访问其他资源时,就可以从Session中的信息来判断用户是否已经登录。
第13行代码 元素是告诉浏览器不要缓存这个页面。
第19~42行代码 是向客户端输出一个登录表单,提交表单的结果将交由loginchk Servlet进行处理。
第 23~27行代码 ,判断从Session中取出的user属性是否为空,如果为空,说明用户还没有登录;如果不为空,说明在这次会话中,用户已经登录过了,我们将已经登陆用户的名字作为输入用户名的文本域的默认值。

step 3: 编写LoginChkServlet 类

  1. public class LoginChkServlet extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp)
  4. throws ServletException, IOException {
  5. req.setCharacterEncoding("gb2312");
  6. String name = req.getParameter("user");
  7. String pwd = req.getParameter("password");
  8. if (name == null || pwd == null || pwd.equals("")) {
  9. resp.sendRedirect("/itguigu/sxlogin");
  10. return;
  11. }else {
  12. HttpSession session = req.getSession();
  13. session.setAttribute("user", name);
  14. resp.sendRedirect("/itguigu/sxloginGreet");
  15. return;
  16. }
  17. }
  18. @Override
  19. protected void doPost(HttpServletRequest req, HttpServletResponse resp)
  20. throws ServletException, IOException {
  21. doGet(req, resp);
  22. }
  23. }

step4:编写一个GreetServlet

  1. public class GreetServlet extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp)
  4. throws ServletException, IOException {
  5. HttpSession session = req.getSession();
  6. String user = (String)session.getAttribute("user");
  7. if (user == null) {
  8. resp.sendRedirect("/itguigu/sxlogin");
  9. }else {
  10. resp.setContentType("text/html;charset=gb2312");
  11. final PrintWriter out = resp.getWriter();
  12. out.println("<html><head><title>欢迎页面</title></head>");
  13. out.println("<body>");
  14. OutputSessionInfo.printSessionInfo(out, session);
  15. out.println("<p>");
  16. out.println("欢迎你," + user + "<p>");
  17. out.println("<a href=/itguigu/sxlogin> 重新登录 </a>");
  18. out.println("<a href=/itguigu/sxlogout> 注销 </a>");
  19. out.println("</body></html>");
  20. out.close();
  21. }
  22. }
  23. }

step5: 编写LogoutServlet 类

  1. public class LogoutServlet extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp)
  4. throws ServletException, IOException {
  5. resp.setContentType("text/html;charset=gb2312");
  6. HttpSession session = req.getSession();
  7. /**服务器是session 失效**/
  8. session.invalidate();
  9. final PrintWriter out = resp.getWriter();
  10. out.println("<html><head><title>退出登录</title></head>");
  11. out.println("<body>");
  12. out.println("已退出登录<br>");
  13. out.println("<a href=/itguigu/sxlogin>重新登录</a>");
  14. out.println("</body></html>");
  15. out.close();
  16. }
  17. }

第8行代码,得到和请求相联系的HttpSession对象。
代码10行 调用HttpSession的方法invalidate()方法使用会话失效。

step 6 : 部署 Servlet

  1. <!--servlet/jsp 深入详解 基于Tomcat的web开发-end -->
  2. <servlet>
  3. <!--登录页面-->
  4. <servlet-name>sxlogin</servlet-name>
  5. <servlet-class>cn.itguigu.servlet.LoginSerlvet</servlet-class>
  6. </servlet>
  7. <servlet-mapping>
  8. <servlet-name>sxlogin</servlet-name>
  9. <url-pattern>/itguigu/sxlogin</url-pattern>
  10. </servlet-mapping>
  11. <servlet>
  12. <!--登录检查-->
  13. <servlet-name>sxloginChk</servlet-name>
  14. <servlet-class>cn.itguigu.servlet.LoginChkServlet</servlet-class>
  15. </servlet>
  16. <servlet-mapping>
  17. <servlet-name>sxloginChk</servlet-name>
  18. <url-pattern>/itguigu/sxloginChk</url-pattern>
  19. </servlet-mapping>
  20. <servlet>
  21. <!--登录欢迎页面-->
  22. <servlet-name>sxloginGreet</servlet-name>
  23. <servlet-class>cn.itguigu.servlet.GreetServlet</servlet-class>
  24. </servlet>
  25. <servlet-mapping>
  26. <servlet-name>sxloginGreet</servlet-name>
  27. <url-pattern>/itguigu/sxloginGreet</url-pattern>
  28. </servlet-mapping>
  29. <servlet>
  30. <!--登出页面-->
  31. <servlet-name>sxlogout</servlet-name>
  32. <servlet-class>cn.itguigu.servlet.LogoutServlet</servlet-class>
  33. </servlet>
  34. <servlet-mapping>
  35. <servlet-name>sxlogout</servlet-name>
  36. <url-pattern>/itguigu/sxlogout</url-pattern>
  37. </servlet-mapping>

step 7: 配置 Web应用程序的运行目录

在%CATALINA_HOME%\conf\Catalina\localhost\目录下新建ch05.xml文件,输入如例5-7所示的内容

例:5-7 ch05.xml

step 8: 访问 Servlet

启动 Tomcat 服务器,打开IE浏览器,首先在地址栏中输入 http://localhost:8080/Javaweb/itguigu/sxlogin 你将看到如图5-2 所示的页面。
image.png

从图中可以看到,当一个用户初次访问login时,服务器建立了会话,但此时客户端还没加入会话,所以会话的状态的状态是新的,会话的创建时间和上次访问时间是相同的,最大的不活动时间间隔是300秒。要注意在web.xml文件中设置Session的超时值时,使用的时间单位是分钟,而getMaxInactiveInterval()方法返回的时间是以秒为单位的。
读者可以按F5键刷新页面,将看到如图5-3所示的页面。
image.png
任意输入一个用户名和密码,单机“登录”提交按钮,将看到如图5-4所示的页面。
从图5-2、图5-3、图5-4中相同的会话ID可以看出,访问的用户在同一个会话中,单击“重新登录”链接,将看到如图5-5所示的页面。
image.png
image.png
在图5-5可以看到,因为在本次会话中,已经有用户登录过,所以程序中将用户上一次登录时输入的用户名显示在了文本域中,具体代码参加例5-2 LoginServlet.java。换一个用户名登录,将看到如图5-6所示的页面。
注意图中的会话ID,可以看到,这一次会话仍然没有结束。单击“注销”链接,出现如图5-7所示的页面。
image.png
image.png
通过在5-5LogoutServlet.java 的session.invalidate()来实现退出登录的。再次单击“重新登录”链接,你会发现开始了一次新的会话。
如果你5分钟没有发送请求,5分钟后,当你刷新页面时,你将看到一次新的会话开始了。
读者可以打开两个不同厂商的浏览器同时访问LoginServlet,将看到服务器创建了两个会话,每个会话都有一个HttpSession对象,如图5-8所示。image.png

step 9 : 在IE浏览器中禁用 Cookie

在前面的步骤中,我们是通过在客户端的浏览器中保存一个名为JSESSIONID 的Cookie来实现的会话跟踪,现在请读者在浏览器中禁用 Cookie,看看我们的例子程序还能否正常工作。
省略 浏览器设置 ………………..

step 10: 重启服务器 再次访问 Servlet

打开浏览器地址栏输入访问地址http://localhost:8080/Javaweb/itguigu/sxlogin
① 多次刷新,每次刷新都会产生一个新的Session对象。
② 如图5-10 浏览器禁用Cookie 后,第一次访问服务器Servlet容器为其生成一个Session对象,并返回给客户端,这时客户端还没有加入会话(因为客户端还没有携带Cookie中的会话标识访问服务器)。随便输入用户名和密码,单击“登录”按钮,又回到了登录页面,如图5-11 所示。
image.png
image.png

从图5-11中可以看到会话的状态是“新的会话”,会话ID 和 图5-10中的会话ID不同。这是因为客户端的浏览器禁用了Cookie,所以Servlet 容器无法从客户端获取到标识会话的Session ID,对于客户端的每次请求,服务器都将创建一个新的HttpSession对象。当客户端登陆后被重定向到GreetServlet.java 的Servlet处理,req.getSession()返回的是一个新的HttpSession对象,从Session中没有得到user属性。因为user对象为空,所以在GreetServlet.java 中调用sendRedirect()方法,又将客户端重定向到了loginServlet。可以看到,当客户端禁用Cookie后,基于Cookie的会话跟踪机制就失效了,自然也就无法跟踪用户的会话和保存用户的状态了。下面我们使用URL重写机制对用户的会话进行跟踪。

step 11: 利用 URL 重写机制跟踪用户会话

① 改造代码
② 将代码中的 请求访问 链接地址、跳转重定向地址 使用响应对象的 encodeURL(); 或者 encodeRedirectURL()方法,

  1. resp.sendRedirect(resp.encodeRedirectURL(req.getContextPath() + "/itguigu/sxUrlRewritelogin"));
  2. resp.encodeURL(req.getContextPath() + "/itguigu/sxUrlRewritelogin");

step 12: 测试URL 重写机制跟踪用户会话分析

输入用户名和密码,点击“登录”按钮,可以看到如图5-12所示的页面。
image.png
注意图 5-12 中 Session ID 作为请求 URL 的一部分被发送到服务器,服务器根据这个Session ID 就可以跟踪用户的会话了。读者还可以看到URL中的 Session ID 和页面中显示的会话ID 是相同的,表明现在的请求和响应都是在同一会话中进行。读者可以点击“重新登录”链接,会看到用户的名字“liuyaguang” 出现在输入用户名的文本域中,说明用户的状态保存下来了,同时Session ID 也作为URL 的一部分出现在地址栏中,如图5-13所示。
image.png

输入用户名和密码登录,单击“注销”链接,再单击“重新登录”链接,出现如图5-14所示的页面。
image.png
我们注意到地址栏中显示的URL 没有附加Session ID ,这是因为我们再LogoutServlet.java 中调用了: session.invalidate(); 方法使会话失效,所以当用户单击“重新登录”链接后,又开始了一个新的会话。
在使用URL重写机制的时候要注意,为了保证会话跟踪的正确性,所有的链接和重定向语句中的URL都需要调用encodeURL()或encodeRedirectURL()方法进行编码。另外,由于附加在URL中的Session ID是动态产生的,对每一个用户都是不同的,所以对于静态页面的互相跳转,URL重写机制就无能为力了,当然,我们也可以将静态页面转换为动态页面来解决这个问题。
重点:
读者也许会考虑,在开发Web应用程序的时候,如何去判断客户端是否禁用了Cookie,从而决定是否采用URL重写的机制去跟踪用户的会话。实际上,客户端是否禁用了Cookie,不需要我们去判断,Servlet容器会帮我们做这件事情。我们在开发Web应用程序的时候,只需要对所有链接和重定向语句中URL都调用encodeURL() 和 encodeRedirectURL()方法进行编码就可以了。
当 Servlet容器 看到一个对 getSession() 方法的调用时,而且它从客户端的请求中没有得到会话ID(Session ID)时 (或者是客户端还保留上次生成的 Session ID 本次请求时携带着发送到服务器端,但是 在Servlet 容器中对应的 HttpSession对象已经失效不存在了),它就知道必须尝试与客户端建立一个新的会话。此时,Serlvet容器并不知道 客户端浏览器的Cookie机制是否工作,所以在向客户返回第一个响应时,它会同时使用Cookie 和 URL 重写这两种机制不仅在响应中发送 Set-Cookie 报头,而且会向 URL 中附加 Session ID (假设这个URL 使用了response.encodeURL()/或response.encodeRedirectURL()进行编码,如果没有调用该方法,当然也就不会有URL重写)。如果客户发出了下一个请求,那么在请求中会包含 Session ID Cookie,同时在 URL中也会附加 Session ID。
当Servlet调用 request.getSession()时,容器首先尝试从Cookie中获取Session ID ,它发现能够得到Session ID,于是就知道这个客户端接收cookie,那么在随后的响应中,它就会使用Cookie来跟踪会话。如果容器从Cookie中没有得到Session ID ,而从URL 中得到 Session ID ,于是在随后的响应中将使用 URL 重写的机制来跟踪会话。
而在encodeURL()和encodeRedirectURL()方法的实现中,大致判断逻辑流程 :
① 他们首先会判断当前的Servlet 是否执行了HttpSession对象的invalidate()方法,如果已经执行了,说明Session对象失效没有Session ID,直接返回参数URL。
② 如果没有调用invalidate() 方法 接下来,判断客户端是否禁用了Cookie?
如果没有禁用,则直接返回参数URL;
如果禁用,则在参数URL中附加Session ID,返回编码后的URL;

了解了 Servlet容器 跟踪 会话的机制以及encodeURL()和encodeRedirectURL() 方法的工作原理,就可以结合基于Cookie 和 URL 重写机制来跟踪用户会话。如果一个web应用程序的功能实现依赖于用户会话的跟踪,那么你可以将所有的页面实现为动态的,并在代码中使用 URL重写机制。在运行时,Servlet容器自动根据客户端的情况来选择会话跟踪的机制。

5.2.3 Cookie 的应用

1、Cookie 类

在Java Servlet API 中给我们提供了 Javax.servlet.http.Cookie 类, 用于创建Cookie。在这个类中,主要有下列方法:
image.png

2、使用 Cookie 的实例

在这一节,我们编写一个登录程序,这个程序将不使用Session 对象而使用Cookie对象来保存用户的登录信息。备注:【原来是使用Cookie 保存了会话ID标识(容器创建的JSESSIONID),用户的登录信息 保存在服务器端HttpSession对象中session.setAttribute(“user”,”liuyaguang”) 】; 现在使用Cookie将用户登录信息)
当用户初次登录时,要求输入用户名和密码,验证通过后,利用Cookie将用户名和密码保存到客户端机器的硬盘上。当用户再次访问页面时,浏览器会将先前保存的Cookie和请求一起发送到服务器,服务器端的web应用程序从Cookie中取出用户名和密码进行验证,验证通过后,向用户显示欢迎信息。在这个程序中,用户只需要登录一次,以后就可以直接访问页面了,这种登录方式,也是网上大多数论坛所采用的方式。实例的开发主要有下列步骤(记住再浏览器中启用Cookie)。

开发步骤:
step 1 : 编写LoginServlet2.java 和 GreetServlet2.java

  1. public class LoginServlet2 extends HttpServlet {
  2. @Override
  3. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
  4. String action=req.getParameter("action");
  5. if("chk".equals(action)) {
  6. String name=req.getParameter("user");
  7. String pwd=req.getParameter("password");
  8. if((name!=null) && (pwd!=null)) {
  9. if(name.equals("zhangsan") && pwd.equals("1234")) {
  10. StringBuffer sb=new StringBuffer();
  11. sb.append("username-");
  12. sb.append(name);
  13. sb.append("&password-");
  14. sb.append(pwd);
  15. Cookie cookie=new Cookie("userinfo",sb.toString());
  16. cookie.setMaxAge(1800);
  17. resp.addCookie(cookie);
  18. resp.sendRedirect(req.getContextPath() + "/itguigu/greet2");
  19. return;
  20. } else {
  21. resp.setContentType("text/html;charset=gb2312");
  22. PrintWriter out=resp.getWriter();
  23. out.println("用户名或密码错误,请<a href="+req.getContextPath() +"/itguigu/login2>重新登录</a>");
  24. return;
  25. }
  26. }
  27. } else {
  28. resp.setContentType("text/html;charset=gb2312");
  29. PrintWriter out=resp.getWriter();
  30. out.println("<html>");
  31. out.println("<meta http-equiv=\"Pragma\" content=\"no-cache\">");
  32. out.println("<head><title>登录页面</title></head>");
  33. out.println("<body>");
  34. out.println("<p>");
  35. out.println("<form method=post action="+ req.getContextPath() +"/itguigu/login2?action=chk>");
  36. out.println("<table>");
  37. out.println("<tr>");
  38. out.println("<td>请输入用户名</td>");
  39. out.println("<td><input type=text name=user></td>");
  40. out.println("</tr>");
  41. out.println("<tr>");
  42. out.println("<td>请输入密码</td>");
  43. out.println("<td><input type=password name=password></td>");
  44. out.println("</tr>");
  45. out.println("<tr>");
  46. out.println("<td><input type=reset value=重置></td>");
  47. out.println("<td><input type=submit value=登录></td>");
  48. out.println("</tr>");
  49. out.println("</table></form></body></html>");
  50. out.close();
  51. }
  52. }
  53. @Override
  54. public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
  55. doGet(req,resp);
  56. }
  57. }
  1. public class GreetServlet2 extends HttpServlet {
  2. @Override
  3. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
  4. Cookie[] cookies=req.getCookies();
  5. if(null!=cookies && cookies.length!=0) {
  6. String name=null;
  7. String pwd=null;
  8. for(int i=0;i<cookies.length;i++) {
  9. Cookie cookie=cookies[i];
  10. String cName=cookie.getName();
  11. if(cName.equals("userinfo")) {
  12. String cValue=cookie.getValue();
  13. String[] userInfo=cValue.split("&");
  14. for(int j=0;j<userInfo.length;j++) {
  15. String[] value=userInfo[j].split("-");
  16. System.out.println(value[0]);
  17. if(value[0].equals("username")) {
  18. name=value[1];
  19. }
  20. if(value[0].equals("password")) {
  21. pwd=value[1];
  22. }
  23. }
  24. }
  25. }
  26. if("zhangsan".equals(name) && "1234".equals(pwd)) {
  27. resp.setContentType("text/html;charset=gb2312");
  28. PrintWriter out=resp.getWriter();
  29. out.println("<html>");
  30. out.println("<meta http-equiv=\"Pragma\" content=\"no-cache\">");
  31. out.println("<head><title>欢迎页面</title></head>");
  32. out.println("<body>");
  33. out.println(name + ",欢迎你");
  34. out.println("<a href="+ req.getContextPath() +"/itguigu/login2>重新登录</a>");
  35. out.println("</body></html>");
  36. out.close();
  37. return;
  38. }
  39. }
  40. RequestDispatcher rd=req.getRequestDispatcher(req.getContextPath() + "/itguigu/login2");
  41. rd.forward(req,resp);
  42. }
  43. }

image.png

image.png

3、总结:

① Session 是指HttpSession创建的对象,是一种服务器端技术,Session对象在服务器端创建。
② Cookie 是由服务器发送给客户端的片段信息,存储在客户端浏览器的内存中或硬盘上,在客户随后对该服务器的请求中 带上发回。
③ 思考一个问题, Cookie 保存到磁盘还是保存在浏览器内存中,由什么来控制?
④ 重要 ! 要区别Cookie的用途,通常有两种:
1、用于会话跟踪的Cookie叫做会话Cookie,在Servlet规范中,用于会话跟踪的Cookie的名字必须是JSESSIONID ,它通常保存在浏览器的内存中。在浏览器内存中的会话Cookie不能被不同的浏览器进程所共享。
eg: 在客户端访问服务器Servlet 时,servlet容器发现只要是调用了getSession()/getSession(true)方法,容器会自动帮我们创建一个HttpSession对象,并将 Session ID 通过设置响应信息(JSESSIONID=CA83F8A287B4A1499C0969375AAED0EC; Path=/Javaweb; HttpOnly )发送给客户端 浏览器,存储在浏览器内存中【为什么存储在内存?因为Serlvet容器创建Cookie时设置超时时间为 -1 ,cookie.setMaxAge(-1) 具体要看Tomcat 实现】,浏览器关闭 会话 ID 消失。
2、第二种是“使用Cookie的实例对象”保存客户的登录信息以及状态信息等。例如:当用户初次登录时,要求输入用户名和密码,验证通过后,利用Cookie将用户名和密码保存到客户端机器的硬盘上或者浏览器的内存中。通常是服务器通过 response.addCookie( new Cookie(“”,”” ))方法,生成Cookie的实例对象 发送给客户端,通过设置过期时间来控制保存到客户端机器磁盘上面还是浏览器内存中。

image.png

5.3 Session 的持久化

image.png

Session 失效的 方式

① 服务器端 调用 HttpSession对象的 invalidate()方法 来实现退出登录;
② 服务器端 web.xml 文件中 设置的Session 过期时间到达,自动失效;

一 、cookie

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

二 、session

image.png

image.png
image.png
image.png

image.png

image.png

image.png
image.png

image.png

image.png

image.png

image.png

image.png

三、相对路径 && 绝对路径

先看问题图1-1 ,再看 图1-3 “/” 在Javaweb 的到底代表什么? 再看图1-2 ,解决编写问题;

image.png
image.png
image.png
图1-3 “/” 在Java web中到底代表什么?
用总结的话说就是 :
① 若 “/” 需要交由Servlet容器来处理时,代表的当前web应用的根路径;
② 若 “/” 需要交由浏览器来处理时, 代表web站点 的根路径;

四、表单重复提交&& 解决表单重复提交

image.png

image.png
image.png
image.png