07.会话及其会话技术
一、什么是会话
当用户通过浏览器访问Web应用时,服务器需要对客户的状态进行跟踪,服务器跟踪用户信息的技术称为会话技术。
我们可以把会话理解为客户端与服务器之间的一次会晤,在一次会晤中可能会包含多次请求和响应。
例如,当你给10086打个电话,你就是客户端,而10086服务人员就是服务器的Servlet了。
从双方接通电话那一刻起,会话就开始了,这之间的你问我答的过程就是一个会话,直到某一方挂断电话表示会话结束。
在通话过程中,你会向10086发出多个请求,那么这多个请求都在一个会话中。
同样,在JavaWeb中,客户端向某一服务器发出第一个请求开始,会话就开始了,直到客户关闭了浏览器会话才结束。
所以说,会话就是一个客户端(浏览器)与Web服务器之间连续发生的一系列请求和响应过程。
在一个会话的多个请求中共享数据(域对象),这就是会话跟踪技术。
例如,一个用户在某网站上的整个购物过程就是一个会话。
用户甲和乙分别登录了购物网站,甲购买了一个一部华为手机,乙购买了一台联想电脑,当这两个用户结账时,Web服务器需要对用户甲和乙的信息分别进行保存。
在之前学习的域对象中,HttpServletRequest对象和ServletContext对象都可以对数据进行保存,但是这两个对象在处理这个问题上都不可行,具体原因如下:
(1)客户端请求Web服务器时,针对每次HTTP请求,Web服务器都会创建一个HttpServletRequest对象,该对象只能保存本次请求所传递的数据。由于购买和结账是两个不同的请求,因此,在发送结账请求时,之前购买请求中的数据将会丢失。
(2)使用ServletContext对象保存数据时,由于同一个Web应用共享的是同一个ServletContext对象,因此,当用户在发送结账请求时,由于无法区分哪些商品是哪个用户所购买的,而会将该购物网站中所有用户购买的商品进行结算,这显然也是不可行的。
为了保存会话过程中产生的数据,在Servlet技术中,提供了两个用于保存会话数据的对象,分别是Cookie和HttpSession。
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的值发生变化,AAA,BBB,CCC 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。
- Cookie的path并不是设置这个Cookie在客户端的保存路径;
- Cookie的path由服务器创建Cookie时设置,Servlet的路径作为Cookie的默认path;
- 当浏览器访问服务器某个路径时,需要归还哪些Cookie给服务器,这由Cookie的path决定;
- 浏览器访问服务器的路径,如果包含某个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/> 密 码:<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/> 密 码:<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是毫秒。