07.会话及其会话技术

一、什么是会话

当用户通过浏览器访问Web应用时,服务器需要对客户的状态进行跟踪,服务器跟踪用户信息的技术称为会话技术。

我们可以把会话理解为客户端与服务器之间的一次会晤,在一次会晤中可能会包含多次请求和响应。

例如,当你给10086打个电话,你就是客户端,而10086服务人员就是服务器的Servlet了。

从双方接通电话那一刻起,会话就开始了,这之间的你问我答的过程就是一个会话,直到某一方挂断电话表示会话结束。

在通话过程中,你会向10086发出多个请求,那么这多个请求都在一个会话中。

同样,在JavaWeb中,客户端向某一服务器发出第一个请求开始,会话就开始了,直到客户关闭了浏览器会话才结束。

所以说,会话就是一个客户端(浏览器)与Web服务器之间连续发生的一系列请求和响应过程。

在一个会话的多个请求中共享数据(域对象),这就是会话跟踪技术。

例如,一个用户在某网站上的整个购物过程就是一个会话。

用户甲和乙分别登录了购物网站,甲购买了一个一部华为手机,乙购买了一台联想电脑,当这两个用户结账时,Web服务器需要对用户甲和乙的信息分别进行保存。

在之前学习的域对象中,HttpServletRequest对象和ServletContext对象都可以对数据进行保存,但是这两个对象在处理这个问题上都不可行,具体原因如下:

(1)客户端请求Web服务器时,针对每次HTTP请求,Web服务器都会创建一个HttpServletRequest对象,该对象只能保存本次请求所传递的数据。由于购买和结账是两个不同的请求,因此,在发送结账请求时,之前购买请求中的数据将会丢失。

(2)使用ServletContext对象保存数据时,由于同一个Web应用共享的是同一个ServletContext对象,因此,当用户在发送结账请求时,由于无法区分哪些商品是哪个用户所购买的,而会将该购物网站中所有用户购买的商品进行结算,这显然也是不可行的。

为了保存会话过程中产生的数据,在Servlet技术中,提供了两个用于保存会话数据的对象,分别是CookieHttpSession

Cookie通过在客户端记录信息确定用户身份,HttpSession通过在服务器端记录信息确定用户身份。

关于Cookie和HttpSession的相关知识,将在下面进行详细讲解。

二、Cookie

1、什么是Cookie

Cookie翻译成中文是“小甜点,小饼干”的意思,它是服务器和客户端之间传输的小数据。

举个例子,在现实生活中,当顾客在购物时,商场经常会赠送顾客一张会员卡,卡上记录用户的个人信息(姓名,手机号、消费记录和积分等),顾客有了会员卡后,每次光临该商场时,都可以使用这张会员卡,商场也将根据会员卡上的消费记录计算会员的优惠额度和累计积分。

在Web应用中,Cookie的功能类似于这张会员卡,当用户通过浏览器访问Web服务器时,服务器会给客户端发送一些信息,这些信息都保存在Cookie中。

当该浏览器再次访问同一服务器时,都会在请求头中将Cookie发送给服务器,方便服务器对浏览器做出正确的响应。

简而言之,Cookie就是一个键和一个值构成的键值对(一键一值),随着服务器端的响应发送给客户端浏览器,然后客户端浏览器会把Cookie保存起来,当下一次再访问同一服务器时把Cookie再发送给服务器。

2、Cookie规范

  • Cookie大小上限为4KB;
  • 一个服务器最多在客户端浏览器上保存20个Cookie;
  • 一个浏览器最多保存300个Cookie;

上面的数据只是HTTP的Cookie规范,但在浏览器大战的今天,一些浏览器为了打败对手,为了展现自己的能力,可能对Cookie规范“扩展”了一些,例如每个Cookie的大小为8KB,最多可保存500个Cookie等。

注意,不同浏览器之间是不共享Cookie

也就是说在你使用IE访问服务器时,服务器会把Cookie发给IE,然后由IE保存起来,当你在使用FireFox访问服务器时,不可能把IE保存的Cookie发送给服务器。

3、Cookie与HTTP头

Cookie是通过HTTP请求头和响应头在客户端和服务器端传递的。

(1)Cookie请求头:客户端发送给服务器端。

格式:Cookie: a=A; b=B; c=C。即多个Cookie用分号隔离开,如下图所示。

(2)Set-Cookie响应头:服务器端发送给客户端。

一个Cookie对象一个Set-Cookie,格式:

Set-Cookie: a=A

Set-Cookie: b=B

Set-Cookie: c=C

 

 

 

4、Cookie的覆盖(一键一值)

如果服务器端发送重复的Cookie那么会覆盖原有的Cookie,例如,客户端的第一个请求,服务器端发送的Cookie是:Set-Cookie: a=A;第二请求,服务器端发送的是:Set-Cookie: a=AA,那么客户端只留下一个Cookie,即:a=AA。

我们来实验一下,创建一个名为HelloSession的项目。

在其中创建一个名为AServlet的Servlet类,其功能是创建三个Cookie保存到客户端浏览器中,代码如下:

@WebServlet("/AServlet")
public class AServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().println("<h1>保存Cookie</h1>");
        Cookie cookie1 = new Cookie("a", "A"); // 创建Cookie
        response.addCookie(cookie1); // 发送给客户端
        Cookie cookie2 = new Cookie("b", "B"); // 创建Cookie
        response.addCookie(cookie2); // 发送给客户端
        Cookie cookie3 = new Cookie("c", "C"); // 创建Cookie
        response.addCookie(cookie3); // 发送给客户端
    }
}

浏览器地址栏输入http://localhost:8080/HelloSession/AServlet,出现如下画面:

使用Chrome浏览器也可以查看Cookie。

再创建一个名为BServlet的Servlet类,其功能是获取客户端浏览器保存的Cookie并显示出来,代码如下:

@WebServlet("/BServlet")
public class BServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().println("<h1>获取Cookie</h1>");
        Cookie[] cookies = request.getCookies(); // 获取Cookie数组
        if (cookies != null) {
            for (Cookie cookie : cookies) {
              response.getWriter().println(cookie.getName()+" = "+cookie.getValue()+"<br>");
              Cookie newCookie = new Cookie(cookie.getName(), cookie.getValue() + cookie.getValue()); // 让Cookie的值发生变化,AAA,BBB,CCC
              response.addCookie(newCookie); // Cookie覆盖
            }
        }
    }
}

浏览器地址栏输入http://localhost:8080/HelloSession/BServlet,出现如下画面:

 

5、Cookie的生命

Cookie不只是有name和value,Cookie还有生命。

所谓生命就是Cookie在客户端的有效时间,可以通过setMaxAge(int)来设置Cookie的有效时间。

  • cookie.setMaxAge(-1):Cookie的maxAge属性的默认值就是-1,表示只在浏览器内存中存活。一旦关闭浏览器窗口,那么Cookie就会消失。
  • cookie.setMaxAge(60 * 60):表示Cookie对象可存活1小时。当生命大于0时,浏览器会把Cookie保存到硬盘上,就算关闭浏览器,就算重启客户端电脑,Cookie也会存活1小时;
  • cookie.setMaxAge(0):Cookie生命等于0是一个特殊的值,它表示Cookie被作废!也就是说,如果原来浏览器已经保存了这个Cookie,那么可以通过Cookie的setMaxAge(0)来删除这个Cookie。无论是在浏览器内存中,还是在客户端硬盘上都会删除这个Cookie。

【案例】显示用户上次访问时间LastAccessServlet.java

思路:

  • 通过创建Cookie,名为lastAccess,值为当前时间,添加到response中,保存到客户端;
  • 在LastAccessServlet中获取请求中名为lastAccess的Cookie;
  • 如果不存在,输出“您是首次访问本站!”,如果存在,输出“您上次访问的时间是:xxx”。

代码:

@WebServlet("/LastAccessServlet")
public class LastAccessServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String lastAccessTime = null;
        Cookie[] cookies = request.getCookies(); // 获取所有的Cookies
        // 遍历cookies数组
        for (int i = 0; cookies != null && i < cookies.length; i++) {
            if ("lastAccess".equals(cookies[i].getName())) {
                // 如果cookie的名称为lastAccess,则获取该cookie的值
                lastAccessTime = URLDecoder.decode(cookies[i].getValue(), "UTF-8"); //对URL进行解码
                break;
            }
        }
        // 判断是否访问过本站
        response.setContentType("text/html;charset=utf-8");
        if (lastAccessTime == null) {
            response.getWriter().println("您是首次访问本站!");
        } else {
            response.getWriter().println("您上次访问的时间是:" + lastAccessTime);
        }
        // 创建cookie,将当前时间作为cookie中的值发送给客户端
        String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        Cookie cookie = new Cookie("lastAccess", URLEncoder.encode(currentTime, "UTF-8")); // 创建一个名为lastAccess的cookie,因为时间格式中有空格,所以对URL进行编码。
        cookie.setMaxAge(60 * 60); // 设置Cookie的生命1小时,这里的单位是秒
        response.addCookie(cookie); // 发送cookie
    }
} 

发布到服务器,地址栏输入http://localhost:8080/HelloSession/LastAccessServlet,如图所示:

 

再次请求刚才的地址,如图所示:

这样,我们的这个Cookie就会在客户端硬盘中保存1小时。

【注意】客户端保存Cookie与浏览器的设置有关

  • 不能在“退出时删除浏览历史记录”,
  • 不能“阻止所有Cookie”

 

6、Cookie的path

(1)什么是Cookie的路径

现在有WEB应用A,向客户端发送了10个Cookie,这就说明客户端无论访问应用A的哪个Servlet都会把这10个Cookie包含在请求中。

但是,也许只有AServlet需要读取请求中的Cookie,而其他Servlet根本就不会获取请求中的Cookie。

这说明客户端浏览器有时发送这些Cookie是多余的!

我们可以通过设置Cookie的path来指定浏览器在访问什么样的路径时,包含哪一些Cookie。

 

  • Cookiepath不是设置这个Cookie在客户端的保存路径;
  • Cookiepath由服务器创建Cookie时设置,Servlet的路径作为Cookie的默认path
  • 当浏览器访问服务器某个路径时,需要归还哪些Cookie给服务器,这由Cookiepath决定;
  • 浏览器访问服务器的路径,如果包含某个Cookie的路径,那么就会归还这个Cookie

(2)Cookie路径与请求路径的关系

下面我们来看看Cookie路径的作用。再原先HelloSession项目的基础上,

再新建CServlet(@WebServlet("/Path1/CServlet"))创建2个Cookie(d、e):

@WebServlet("/Path1/CServlet")
public class CServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().println("<h1>Cookie路径:Path1</h1>");        
        Cookie cookie1 = new Cookie("d", "D");
        response.addCookie(cookie1);        
        Cookie cookie2 = new Cookie("e", "E");
        response.addCookie(cookie2);
    }
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
    }
}

新建DServlet(@WebServlet("/Path1/DServlet"))查看/Path1路径包含的Cookie:

@WebServlet("/Path1/DServlet")
public class DServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().println("<h1>Cookie路径:Path1</h1>");        
        Cookie[] cookies = request.getCookies(); // 获取Cookie数组
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                response.getWriter().println(cookie.getName() + " = " + cookie.getValue() + "<br>");
            }
        }
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}

再新建EServlet(@WebServlet("/Path2/EServlet"))创建2个Cookie(f、g):

@WebServlet("/Path2/EServlet")
public class EServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().println("<h1>Cookie路径:Path2</h1>");        
        Cookie cookie1 = new Cookie("f", "F");
        response.addCookie(cookie1);
        Cookie cookie2 = new Cookie("g", "G");
        response.addCookie(cookie2);
    }
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
    }
}

新建FServlet(@WebServlet("/Path2/FServlet"))查看/Path2路径包含的Cookie:

@WebServlet("/Path2/FServlet")
public class FServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().println("<h1>Cookie路径:Path2</h1>");        
        Cookie[] cookies = request.getCookies(); // 获取Cookie数组
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                response.getWriter().println(cookie.getName() + " = " + cookie.getValue() + "<br>");
            }
        }
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}

这样我们的客户端浏览器中就保存的7个Cookie分别位于3个路径下:

  • cookie a、b、c的路径是:/HelloSession;
  • cookie d、e的路径是:/HelloSession/Path1;
  • cookie f、g的路径是:/HelloSession/Path2;

 下面我们通过几个浏览器请求URL测试一下:

  • 请求http://localhost:8080/HelloSession/AServlet  或者  http://localhost:8080/HelloSession/BServlet  会在请求中包含a、b、c;
  • 请求http://localhost:8080/HelloSession/Path1/CServlet  或者  http://localhost:8080/HelloSession/Path1/DServlet  会在请求中包含a、b、c、d、e;
  • 请求http://localhost:8080/HelloSession/Path2/EServlet  或者  http://localhost:8080/HelloSession/Path2/FServlet  会在请求中包含a、b、c、f、g;

也就是说,请求路径(字符串,不是文件夹的概念)如果包含了Cookie路径,那么会在请求中包含这个Cookie,否则不会包含这个Cookie

(3)设置Cookie的路径

除了可以使用Servlet的路径作为Cookie的默认路径外,我们还可以使用setPath()方法设置Cookie的路径,例如:

cookie.setPath("/HelloSession/Path1"); // /Web应用名/路径名

如果为Cookie设置路径为"/",表示在Tomcat的webapps目录下的所有Web应用共享这个Cookie,例如:cookie.setPath("/")。

7、Cookie中保存中文

Cookie的name和value都不能使用中文。

如果希望在Cookie中使用中文,那么需要先使用java.net.URLEncoder的encode()方法对中文进行URL编码,然后把编码后的字符串放到Cookie中。

在读取Cookie的过程中,再使用java.net.URLDecoder的decode()方法对中文进行URL解码。

向客户端响应中添加Cookie:

String name = java.net.URLEncoder.encode("姓名", "UTF-8");
String value = java.net.URLEncoder.encode("张三", "UTF-8");
Cookie cookie = new Cookie(name, value);
response.addCookie(cookie);

从客户端请求中获取Cookie:

response.setContentType("text/html;charset=utf-8");
Cookie[] cookies = request.getCookies(); // 获取Cookie数组
if(cookies != null) {
    for(Cookie cookie : cookies) {
        String name = java.net.URLDecoder.decode(cookie.getName(), "UTF-8");
        String value = java.net.URLDecoder.decode(cookie.getValue(), "UTF-8");
        response.getWriter().println(name + " : " + value + "<br>");
    }
}

 

三、HttpSession

HttpSession是一个接口,是由JavaWeb提供的,不是Http协议的组成部分,用来进行会话的跟踪。

HttpSession是服务器端对象,保存在服务器端。

HttpSession底层依赖Cookie,或是URL重写。

会话是某个用户从首次访问服务器开始,到该用户关闭浏览器才结束。

表示一个用户对服务器的多次连贯性请求。

所谓连贯性请求,就是该用户多次请求中间没有关闭浏览器。

在一台电脑上打开两个浏览器相当于两个客户端,是两个会话,在服务器端会保存两个HttpSession对象。

服务器会为每个客户端创建一个HttpSession对象,HttpSession就好比客户在服务器端的账户,它们被服务器保存到一个Map中,这个Map被称之为HttpSession缓存。

1、获得HttpSession对象

Servlet中得到HttpSession对象:

HttpSession request.getSesssion():如果当前会话已经有了HttpSession对象那么直接返回,如果当前会话还不存在,那么创建HttpSession对象并返回;

例如:HttpSession session = request.getSession();

HttpSession request.getSession(Boolean create):当参数为true时,与requeset.getSession()相同。如果参数为false,那么如果当前会话中存在HttpSession则返回,不存在返回null

JSP中得到HttpSession对象:

session是JSP内置对象之一,不用创建就可以直接使用。

2、HttpSession的方法

HttpSession是JavaWeb四大域对象之一(PageContext、ServletRequest、HttpSession、ServletContext),所以它也有域对象的4个方法:

  • void setAttribute(String name, Object value)
  • Object getAttribute(String name)
  • void removeAttribute(String name)
  • Enumeration<String> getAttributeNames()

ServletRequest、HttpSession、ServletContext三个是Servlet中可以使用的域对象,而JSP中可以多使用一个域对象PageContext,等学到JSP时再讲。

(1)HttpServletRequest:一个请求创建一个request对象,所以在同一个请求中可以共享request,例如一个请求从AServlet转发到BServlet,那么AServlet和BServlet可以共享request域中的数据;(范围小)

(2)ServletContext:一个应用只创建一个ServletContext对象,所以在ServletContext中的数据可以在整个应用中共享,只要不重启服务器,那么ServletContext中的数据就可以共享;(范围最大)

(3)HttpSession:一个会话创建一个HttpSession对象,同一会话中的多个请求中可以共享session中的数据;(范围中)

下面我们来测试一下HttpSession的用法。

在HelloSession项目中,新建GServlet类,保存数据到HttpSession中,代码如下:

@WebServlet("/GServlet")
public class GServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        session.setAttribute("aaa", "AAA");
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().print("<h1>保存数据到HttpSession中</h1>");
    }
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
    }
}

新建HServlet类,获取HttpSession中的数据,代码如下:

@WebServlet("/HServlet")
public class HServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().print("<h1>获取HttpSession中的数据</h1>");
        response.getWriter().print(session.getAttribute("aaa"));
    }
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
    }
}

发布部署程序到Tomcat中,打开IE浏览器,地址栏输入http://localhost:8080/HelloSession/GServlet,然后,不要关闭当前IE浏览器,新建一个窗口或者在当前窗口,地址栏输入http://localhost:8080/HelloSession/HServlet,可以看到页面上显示出刚才保存的数据“AAA”了。

 

再新建一个IE浏览器,地址栏输入http://localhost:8080/HelloSession/HServlet,页面上就显示null,因为无法获取数据。

 

我们使用F12开发人员工具查看,当在同一个IE浏览器中保存或者获取HttpSession中的数据时,Cookie选项卡中的JSESSIONID是一样的。

但是,新建的IE浏览器中获取HttpSession中的数据时,Cookie选项卡中的JSESSIONID就和之前的是不一样的了。

因为,新建一个IE浏览器就相当于新建了一个会话,另外,我们可以看出,HttpSession底层还是依赖于Cookie的。

【案例】保存用户登录信息

  • 需要的页面:
  •  login.jsp:登录页面,提供登录表单。如果保存过用户名,还原到界面;
  •  welcome.jsp:欢迎页,显示当前用户名称。如果没有登录,显示您还没登录;
  •  about.jsp:关于页,显示当前用户名称。如果没有登录,显示您还没登录;
  • Servlet:
  • LoginServlet:在login.jsp页面提交表单时,请求本Servlet。在本Servlet中获取用户名、密码进行校验,如果用户名、密码错误,显示“用户名或密码错误!”,如果正确,保存用户名到session中,然后重定向到welcome.jsp;

当用户没有登录时,通过地址栏直接访问welcome.jsp或about.jsp,显示“您还没有登录”或者自动跳转到登录界面。

如果用户在login.jsp登录成功后到达welcome.jsp页面会显示当前用户名,而且不用再次登录去访问about.jsp也会显示用户名。

因为多次请求在一个会话范围,welcome.jsp和about.jsp都会到session中获取用户名,session对象在一个会话中是相同的,所以都可以获取到用户名。

login.jsp代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录</title>
</head>
<body>
    <h1>登录页面</h1>
    <hr />
    <%
        String username = (String) session.getAttribute("username");
        if (username == null){
            username = "";
        }
    %>
    <form action="./LoginServlet" method="post">
        用户名:<input type="text" name="username" value="<%=username%>" style="width:150px;height:25px;"/><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;码:<input type="password" name="password" style="width:150px;height:25px;"/><br/><br/>
        <input type="submit" value="登录" />
    </form>
</body>
</html>

welcome.jsp代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>成功</title>
</head>
<body>
    <%
        String username = (String) session.getAttribute("username");
        if (username == null) {
            out.print("<h1>您还没有登录!</h1><hr />");
        } else {
            out.print("<h1>成功页面</h1><hr />");
            out.print("用户名:" + username);
        }
    %>
    <a href="./about.jsp">about</a>
</body>
</html>

about.jsp代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>关于</title>
</head>
<body>
    <%
        String username = (String) session.getAttribute("username");
        if (username == null) {
            out.print("<h1>您还没有登录!</h1><hr />5秒后自动跳转登录界面。");
            response.setHeader("Refresh", "5;URL=/HelloSession/login.jsp");
        } else {
            out.print("<h1>关于页面</h1><hr />");
            out.print("用户名:" + username);
        }
    %>
    <a href="./welcome.jsp">welcome</a>
</body>
</html>

LoginServlet.java代码如下:

@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response); // 一定不要忘记
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        System.out.println("username = " + username);
        System.out.println("password = " + password);
        if ("admin".equalsIgnoreCase(username) && "123".equals(password)) {
            // 登录成功
            HttpSession session = request.getSession();
            session.setAttribute("username", username); // 向HttpSession域中保存用户名
            response.sendRedirect("./welcome.jsp");
        } else {
            // 登录失败
            response.getWriter().print("用户名或密码错误!");
        }
    }
}

3、HttpSession的实现原理

HttpSession是依赖于Cookie技术的。

当客户端访问服务器时,服务器会创建一个HttpSession对象,并且给这个对象起个ID,将HttpSession对象保存在服务器上,将这个ID通过Cookie传递给客户端,就是JSESSIONID(32位随机数)。

举例来说,当我们第一次去商城购物,商城会给我们办个会员卡,这个会员卡上只有卡号,我们的账户信息是建立在商城的数据库服务器上的,以后每次去商城都是出示会员卡,告诉商城卡号,商城就能查找到我们会员积分和记录。

在以上这个例子中:

  • Cookie:就是会员卡,在客户手中;
  • HttpSession:就是数据库里的账号信息,在服务器上;
  • JSESSIONID:就是卡号,客户端与服务器之间的联系。

当客户端浏览器关闭或者超过一定时长不活动(默认30分钟),服务器会自动销毁HttpSession对象。

服务器收到请求后,不会马上创建HttpSession对象,而是在第一次执行request.getSession()方法时,才会创建。

4、HttpSession的其他方法:

  • String getId():获取JSESSIONID;
  • long getCreationTime():获得HttpSession创建时间的毫秒数,这个时间是与1970年1月1日00:00:00之间时间差的毫秒数;
  • long getLastAccessedTime():获得最后一次发送HttpSession的时间毫秒数;
  • int getMaxInactiveInterval():获取HttpSession对象的最大不活动时间(秒),默认为30分钟(1800秒)。当session在30分钟内没有使用,那么Tomcat会在session池中移除这个session;
  • void invalidate():让HttpSession对象失效,当HttpSession对象失效后,客户端再次请求,服务器会给客户端创建一个新的HttpSession对象,并在响应中给客户端新HttpSession对象的JSESSIONID;
  • boolean isNew():查看HttpSession对象是否为新。当客户端第一次请求时,服务器为客户端创建HttpSession对象,但这时服务器还没有响应客户端,也就是还没有把JSESSIONID响应给客户端时,这时HttpSession对象的状态为新。
  • ServletContext getServletContext():获取会话所属的Web应用的ServletContext对象。

【复习1】JavaWeb四大域对象,所有域对象都有存取数据的功能,因为域对象内部有一个Map,用来存储数据。

  • PageContext
  • ServletRequest
  • HttpSession
  • ServletContext

【复习2】获取ServletContext的方法:

  • ServletConfig#getServletContext();
  • GenericServlet#getServletContext();
  • HttpSession#getServletContext()
  • ServletContextEvent#getServletContext()

5、配置session最大不活动时间

(1)void setMaxInactiveInterval(int interval):设置HttpSession对象的最大不活动时间(秒)

(2)web.xml文件中配置

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

如果将时间值设置为0或者一个负数,则表示session永不失效。

6、利用URL重写实现HttpSession跟踪

我们知道HttpSession依赖Cookie,那么HttpSession为什么依赖Cookie呢?因为服务器需要在每次请求中获取JSESSIONID,然后找到客户端的HttpSession对象。那么如果客户端浏览器阻止了所有Cookie呢?那么HttpSession是不是就会不存在了呢?

其实还有一种方法让服务器收到的每个请求中都带有JSESSIONID,那就是URL重写。

所谓URL重写,就是指将在每个页面中的每个链接和表单中都添加名为jsessionid的参数,值为当前JSESSIONID。

当用户点击链接或提交表单时也服务器可以通过获取jsessionid这个参数来得到客户端的JSESSIONID,找到HttpSession对象。

注意jsessionid参数前面是分号“;

  • ./a.jsp;jsessionid=<%=session.getId()%>
  • ./GServlet;jsessionid=<%=session.getId()%>

我们来测试一下,创建一个a.jsp,部分代码如下:

<body>
    <h1>URL重写</h1>
    <a href='./a.jsp;jsessionid=<%=session.getId()%>'>主页</a>
    <form action='./a.jsp;jsessionid=<%=session.getId()%>' method="post">
        <input type="submit" value="提交" />
    </form>
</body>

可以看出,不管是链接,还是表单,我们都人为的加上了“;jsessionid=XXXXXX”这个参数,这样就可以把每个请求都携带上JSESSIONID提交给服务器。

另外,还有更好的方法,我们可以使用response.encodeURL()对每个请求的URL处理,这个方法会自动追加jsessionid参数,与上面我们手动添加是一样的效果。

但是,使用response.encodeURL()更加“智能”,它会判断客户端浏览器是否禁用了Cookie,如果禁用了,那么这个方法在URL后面追加jsessionid,否则不会追加。

<body>
    <h1>URL重写</h1>    
    <a href='<%=response.encodeURL("./a.jsp")%>'>主页</a>
    <form action='./a.jsp;jsessionid=<%=session.getId()%>' method="post">
        <input type="submit" value="提交" />
    </form>
    <%
        out.print(response.encodeURL("./a.jsp"));
    %>
</body>

   

【案例】利用Session实现一次性验证码

思路:

  • JSP页面生成验证码图片,同时,将验证码文本保存到session中;
  • 当用户提交请求时,从请求参数中获取用户输入的验证码;
  • 比较用户输入的验证码与session中真正的验证码,给出提示。

(1)我们之前已经编写了VerifyCode类,这个类可以生成验证码图片。

下面我们要做的就是在JSP页面中显示动态图片。

VerifyCode.java具体代码如下:

public class VerifyCode {
    private int w = 150; // 宽度
    private int h = 70; // 高度
    private Random r = new Random(); // 获得一个随机数对象
    // 定义有哪些字体
    private String[] fontNames = { "宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312" };
    // 定义有哪些验证码的随机字符
    private String codes = "23456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ";
    // 生成背景色
    private Color bgColor = new Color(250, 250, 250);
    // 生成的验证码文本
    private String text;
    // 生成随机颜色
    private Color randomColor() {
        int red = r.nextInt(150);
        int green = r.nextInt(150);
        int blue = r.nextInt(150);
        return new Color(red, green, blue);
    }
    // 生成随机字体
    private Font randomFont() {
        int index = r.nextInt(fontNames.length);
        String fontName = fontNames[index]; // 字体名称
        int style = r.nextInt(4); // 字体样式(PLAIN=0,BOLD=1,ITALIC=2,BOLD|ITALIC=3)
        int size = r.nextInt(5) + 50; // 字体大小(50-54之间)
        return new Font(fontName, style, size);
    }
    // 画干扰线
    private void drawLine(BufferedImage image) {
        int num = 3; // 干扰线数量
        Graphics2D g2 = (Graphics2D) image.getGraphics(); // 得到图片画笔
        for (int i = 0; i < num; i++) {
            int x1 = r.nextInt(w); // 起点x坐标
            int y1 = r.nextInt(h); // 起点y坐标
            int x2 = r.nextInt(w); // 终点x坐标
            int y2 = r.nextInt(h); // 终点y坐标
            g2.setStroke(new BasicStroke(1.5F));// 设置线条特征,1.5F为线的宽度
            g2.setColor(Color.BLUE); // 干扰线颜色
            g2.drawLine(x1, y1, x2, y2); // 画线
        }
    }
    // 得到codes的长度内的随机数,并使用charAt取得随机数位置上的codes中的字符
    private char randomChar() {
        int index = r.nextInt(codes.length());
        return codes.charAt(index);
    }
    // 创建一张验证码的图片
    public BufferedImage createImage() {
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = (Graphics2D) image.getGraphics(); // 得到图片画笔
        g2.setColor(bgColor); // 给画笔设置颜色
        g2.fillRect(0, 0, w, h); // 使用画笔填充指定的矩形
        g2.setColor(Color.RED); // 给画笔设置颜色
        g2.drawRect(0, 0, w - 1, h - 1); // 使用画笔绘制指定的矩形
        StringBuilder sb = new StringBuilder();
        // 向图中画四个字符
        for (int i = 0; i < 4; i++) {
            String s = randomChar() + "";
            sb.append(s);
            float x = i * 1.0F * w / 4; // 计算字符x坐标位置
            g2.setFont(randomFont()); // 设置画笔字体
            g2.setColor(randomColor()); // 设置画笔颜色
            g2.drawString(s, x, h - 20); // 在图片上写字符
        }
        text = sb.toString();
        drawLine(image); // 绘制干扰线
        return image;// 返回图片
    }
    // 得到验证码的文本,后面是用来和用户输入的验证码,检测用
    public String getText() {
        return text;
    }
    // 定义输出的对象和输出文件流
    public void output(BufferedImage bi, OutputStream output) throws IOException {
        ImageIO.write(bi, "JPEG", output);
    }
}

(2)我们编写一个VerifyCodeServlet,在这个Servlet中我们生成动态图片,然后它图片写入到response.getOutputStream()流中。

然后让JSP页面的<img>元素指定这个VerifyCodServlet即可。

VerifyCodServlet.java代码如下:

@WebServlet("/VerifyCodeServlet")
public class VerifyCodeServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/jpeg"); // 设置响应体类型
        VerifyCode code = new VerifyCode();
        BufferedImage image = code.createImage(); // 生成验证码图片
        String text = code.getText(); // 获取验证码文本
        System.out.println("text = " + text);
        // 将验证码文本保存到session中
        request.getSession().setAttribute("sessionVerifyCode", text); 
        code.output(image, response.getOutputStream()); // 将验证码图片输出响应流
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

(3)修改原先的login.jsp页面,增加验证码控件。

代码如下:

<%@ page import="java.util.Date"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录</title>
<script type="text/javascript">
    function _change() {
        var imgEle = document.getElementById("vCode"); // 获得图片控件元素
        imgEle.src = "./VerifyCodeServlet?" + new Date().getTime();
    }
</script>
</head>
<body>
    <h1>登录页面</h1>
    <hr />
    <%
        String username = (String) session.getAttribute("username");
        if (username == null){
            username = "";
        }
    %>
    <form action="./LoginServlet" method="POST">
        用户名:<input type="text" name="username" value="<%=username%>" style="width:150px;height:25px;"/><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;码:<input type="password" name="password" style="width:150px;height:25px;"/><br/><br/> 
        验证码:<input type="text" name="verify_code" maxlength="4" size="4" style="height:25px;" align="middle"/>
        <img id="vCode" src="./VerifyCodeServlet?<%=new Date().getTime() %>" width="65px" height="30px" align="middle" />
        <a href="javascript:_change()">看不清,换一张</a><br/><br/>
        <input type="submit" value="登录" />
    </form>
</body>
</html>

(4)修改LoginServlet,增加验证码判断逻辑。

LoginServlet.java代码如下:

@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response); // 一定不要忘记
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String verifyCode = request.getParameter("verify_code");
        String sessionVerifyCode = (String) request.getSession().getAttribute("sessionVerifyCode");
        System.out.println("username = " + username);
        System.out.println("password = " + password);
        System.out.println("verifyCode = " + verifyCode);
        System.out.println("sessionVerifyCode = " + sessionVerifyCode);
        if ("admin".equalsIgnoreCase(username) && "123".equals(password) && sessionVerifyCode.equalsIgnoreCase(verifyCode)) {
            // 登录成功
            HttpSession session = request.getSession();
            session.setAttribute("username", username); // 向HttpSession域中保存用户名
            response.sendRedirect("./welcome.jsp");
        } else if (!"admin".equalsIgnoreCase(username) || !"123".equals(password)) {
            // 登录失败
            response.getWriter().print("用户名或密码错误!");
        } else {
            response.getWriter().print("验证码错误!");
        }
    }
}

运行程序。

 

可以看出,请求login.jsp页面时,实际上是两个请求:

(1)请求login.jsp页面;

(2)请求验证码图片。

当我们点击“看不清,换一张”的超链接时,会发现在“./VerifyCodeServlet”后面会拼接上当前系统时间的毫秒数(时间戳)。

这是为了避免每次请求的地址是相同的,浏览器就去读取缓存,而不去访问服务器,我们通过使用时间戳来使每次的请求地址都不同,从而跳过浏览器的缓存机制,实现每次都去请求服务器,这种做法在获取图片验证码时是非常常用的。

 

【补充】long型时间转换

Enumeration<String> params = request.getParameterNames();
if (params.hasMoreElements()) {
    String param = params.nextElement();
    long time = Long.parseLong(param);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
    System.out.println(param + " = " + sdf.format( new Date(time) ));
}

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 

转换的格式:yyyy是完整的西元年,MM是月份,dd是日期, 至於HH:mm:ss.SSS 时分秒

注意:有的格式大写,有的格式小写,那是怕避免混淆。

例如,MM是月份,mm是分;

HH是24小时制,而hh是12小时制;

ss是秒,SSS是毫秒。

posted @ 2022-10-24 16:53  熊猫Panda先生  阅读(380)  评论(0编辑  收藏  举报