JavaWeb(六):会话与状态管理
HTTP协议是一种无状态的协议,WEB服务器本身不能识别出哪些请求是同一个浏览器发出的 ,浏览器的每一次请求都是完全孤立的。即使 HTTP1.1 支持持续连接,但当用户有一段时间没有提交请求,连接也会关闭。怎么才能实现网上商店中的购物车呢:某个用户从网站的登录页面登入后,再进入购物页面购物时,负责处理购物请求的服务器程序必须知道处理上一次请求的程序所得到的用户信息。
作为 web 服务器,必须能够采用一种机制来唯一地标识一个用户,同时记录该用户的状态。
WEB应用中的会话是指一个客户端浏览器与WEB服务器之间连续发生的一系列请求和响应过程。WEB应用的会话状态是指WEB服务器与浏览器在会话过程中产生的状态信息,借助会话状态,WEB服务器能够把属于同一会话中的一系列的请求和响应过程关联起来。
WEB服务器端程序要能从大量的请求消息中区分出哪些请求消息属于同一个会话,即能识别出来自同一个浏览器的访问请求,这需要浏览器对其发出的每个请求消息都进行标识:属于同一个会话中的请求消息都附带同样的标识号,而属于不同会话的请求消息总是附带不同的标识号,这个标识号就称之为会话ID(SessionID)。
在 Servlet 规范中,常用以下两种机制完成会话跟踪
- Cookie
- Session
一、Cookie
cookie机制采用的是在客户端保持 HTTP 状态信息的方案,Cookie是在浏览器访问WEB服务器的某个资源时,由WEB服务器在HTTP响应消息头中附带传送给浏览器的一个小文本文件。一旦WEB浏览器保存了某个Cookie,那么它在以后每次访问该WEB服务器时,都会在HTTP请求头中将这个Cookie回传给WEB服务器。
底层的实现原理: WEB服务器通过在HTTP响应消息中增加Set-Cookie响应头字段将Cookie信息发送给浏览器,浏览器则通过在HTTP请求消息中增加Cookie请求头字段将Cookie回传给WEB服务器。
一个Cookie只能标识一种信息,它至少含有一个标识该信息的名称(NAME)和设置值(VALUE)。一个WEB站点可以给一个WEB浏览器发送多个Cookie,一个WEB浏览器也可以存储多个WEB站点提供的Cookie。浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。
1.1 Cookie常用API
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false"%> <!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>Insert title here</title> </head> <body> <% //在 JavaWEB 规范中使用 Cookie 类代表 cookie //1. 获取 Cookie Cookie [] cookies = request.getCookies(); if(cookies != null && cookies.length > 0){ for(Cookie cookie: cookies){ //2. 获取 Cookie 的 name 和 value out.print(cookie.getName() + ": " + cookie.getValue()); out.print("<br>"); } }else{ out.print("没有一个 Cookie, 正在创建并返回"); //1. 创建一个 Cookie 对象 Cookie cookie = new Cookie("name", "aidata"); //setMaxAge: 设置 Cookie 的最大时效, 以秒为单位, 若为 0 , 表示立即删除该 Cookie //若为负数, 表示不存储该 Cookie, 若为正数, 表示该 Cookie 的存储时间. cookie.setMaxAge(30); //2. 调用 response 的一个方法把 Cookie 传给客户端. response.addCookie(cookie); } %> </body> </html>
Servlet API中提供了一个javax.servlet.http.Cookie类来封装Cookie信息,它包含有生成Cookie信息和提取Cookie信息的各个属性的方法。
Cookie类的方法:
- 构造方法: public Cookie(String name,String value)
- getName方法
- setValue与getValue方法
- setMaxAge与getMaxAge方法
- setPath与getPath方法
HttpServletResponse接口中定义了一个addCookie方法,它用于在发送给浏览器的HTTP响应消息中增加一个Set-Cookie响应头字段。
HttpServletRequest接口中定义了一个getCookies方法,它用于从HTTP请求消息的Cookie请求头字段中读取所有的Cookie项。
Cookie的发送
1.创建Cookie对象
2.设置最大时效
3.将Cookie放入到HTTP响应报头
如果创建了一个cookie,并将他发送到浏览器,默认情况下它是一个会话级别的cookie; 存储在浏览器的内存中,用户退出浏览器之后被删除。若希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以秒为单位的时间。将最大时效设为0则是命令浏览器删除该cookie。
发送cookie需要使用HttpServletResponse的addCookie方法,将cookie插入到一个 Set-Cookie HTTP响应报头中。由于这个方法并不修改任何之前指定的Set-Cookie报头,而是创建新的报头,因此将这个方法称为是addCookie,而非setCookie。
会话Cookie和持久Cookie
如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。
存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。
Cookie的读取
1.调用request.getCookies
要获取浏览器发送来的cookie,需要调用HttpServletRequest的getCookies方法,这个调用返回Cookie对象的数组,对应由HTTP请求中Cookie报头输入的值。
2.对数组进行循环,调用每个cookie的getName方法,直到找到感兴趣的cookie为止
1.2 利用Cookie自动登陆
不需要填写用户名和密码等信息,可以自动登录到系统。
登陆页面
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>Insert title here</title> </head> <body> <form action="index.jsp" method="post"> name: <input type="text" name="name"/> <input type="submit" value="Submit"/> </form> </body> </html>
登陆会转向index.jsp
- 若获取的请求参数 loginName不为空, 则打印出欢迎信息。把登录信息存储到 Cookie 中,并设置 Cookie 的最大时效为 30S
- 从 Cookie 中读取用户信息,若存在则打印欢迎信息
- 若既没有请求参数,也没有 Cookie,则重定向到 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>Insert title here</title> </head> <body> <% //若可以获取到请求参数 name, 则打印出欢迎信息。把登录信息存储到 Cookie 中,并设置 Cookie 的最大时效为 30S String name = request.getParameter("name"); if(name != null && !name.trim().equals("")){ Cookie cookie = new Cookie("name", name); cookie.setMaxAge(30); response.addCookie(cookie); }else{ //从 Cookie 中读取用户信息,若存在则打印欢迎信息 Cookie [] cookies = request.getCookies(); if(cookies != null && cookies.length > 0){ for(Cookie cookie : cookies){ String cookieName = cookie.getName(); if("name".equals(cookieName)){ String val = cookie.getValue(); name = val; } } } } if(name != null && !name.trim().equals("")){ out.print("Hello: " + name); }else{ //若既没有请求参数,也没有 Cookie,则重定向到 login.jsp response.sendRedirect("login.jsp"); } %> </body> </html>
1.3 利用Cookie显示最近浏览的商品
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!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>Insert title here</title> </head> <body> <h4>Books Page</h4> <a href="book.jsp?book=JavaWeb">Java Web</a><br><br> <a href="book.jsp?book=Java">Java</a><br><br> <a href="book.jsp?book=Oracle">Oracle</a><br><br> <a href="book.jsp?book=Ajax">Ajax</a><br><br> <a href="book.jsp?book=JavaScript">JavaScript</a><br><br> <a href="book.jsp?book=Android">Android</a><br><br> <a href="book.jsp?book=Jbpm">Jbpm</a><br><br> <a href="book.jsp?book=Struts">Struts</a><br><br> <a href="book.jsp?book=Hibernate">Hibernate</a><br><br> <a href="book.jsp?book=Spring">Spring</a><br><br> <br><br> <% //显示最近浏览的 5 本书 //获取所有的 Cookie Cookie [] cookies = request.getCookies(); //从中筛选出 Book 的 Cookie:如果 cookieName 为 ATGUIGU_BOOK_ 开头的即符合条件 //显示 cookieValue if(cookies != null && cookies.length > 0){ for(Cookie c: cookies){ String cookieName = c.getName(); if(cookieName.startsWith("AIDATA_BOOK_")){ out.println(c.getValue()); out.print("<br>"); } } } %> </body> </html>
显示最近浏览的 5 本书
获取所有的 Cookie
从中筛选出 Book 的 Cookie:如果 cookieName 为 AIDATA_BOOK_ 开头的即符合条件
显示 cookieValue
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!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>Insert title here</title> </head> <body> <h4>Book Detail Page</h4> Book: <%= request.getParameter("book") %> <br><br> <a href="books.jsp">Return</a> <% String book = request.getParameter("book"); //把书的信息以 Cookie 方式传回给浏览器,删除一个 Cookie //1. 确定要被删除的 Cookie: //前提: AIDATA_BOOK_ 开头的 Cookie 数量大于或等于 5, Cookie [] cookies = request.getCookies(); //保存所有的 AIDATA_BOOK_ 开头的 Cookie List<Cookie> bookCookies = new ArrayList<Cookie>(); //用来保存和 books.jsp 传入的 book 匹配的那个 Cookie Cookie tempCookie = null; if(cookies != null && cookies.length > 0){ for(Cookie c: cookies){ String cookieName = c.getName(); if(cookieName.startsWith("AIDATA_BOOK_")){ bookCookies.add(c); if(c.getValue().equals(book)){ tempCookie = c; } } } } //①. 且若从 books.jsp 页面传入的 book 不在 AIDATA_BOOK_ 的 Cookie 中则删除较早的那个 Cookie //( ATGUIGU_BOOK_ 数组的第一个 Cbookie), if(bookCookies.size() >= 5 && tempCookie == null){ tempCookie = bookCookies.get(0); } //②. 若在其中,则删除该 Cookie if(tempCookie != null){ tempCookie.setMaxAge(0); response.addCookie(tempCookie); } //2. 把从 books.jsp 传入的 book 作为一个 Cookie 返回 Cookie cookie = new Cookie("AIDATA_BOOK_" + book, book); response.addCookie(cookie); %> </body> </html>
把书的信息以 Cookie 方式传回给浏览器,删除一个 Cookie
确定要被删除的 Cookie: AIDATA_BOOK_ 开头的 Cookie 数量大于或等于 5,且若从 books.jsp 页面传入的 book 不在 ATGUIGU_BOOK_ 的 Cookie 中则删除较早的那个 Cookie( AIDATA_BOOK_ 数组的第一个 Cbookie),若在其中,则删除该 Cookie
把从 books.jsp 传入的 book 作为一个 Cookie 返回
1.4 设置Cookie作用路径
功能:Cookie的作用范围
cookie2.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>Insert title here</title> </head> <body> <!-- 读取一个 name 为 cookiePath 的 Cookie --> <% String cookieValue = null; Cookie [] cookies = request.getCookies(); if(cookies != null && cookies.length > 0){ for(Cookie cookie: cookies){ if("cookiePath".equals(cookie.getName())){ cookieValue = cookie.getValue(); } } } if(cookieValue != null){ out.print(cookieValue); }else{ out.print("没有指定的Cookie."); } %> </body> </html>
<%@ 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>Insert title here</title> </head> <body> <!-- 向客户端浏览器写入一个 Cookie: cookiePath, cookiePathValue --> <% Cookie cookie = new Cookie("cookiePath", "CookiePathValue"); //设置 Cookie 的作用范围: cookie.setPath(request.getContextPath()); response.addCookie(cookie); //Cookie 的 作用范围: 可以作用当前目录和当前目录的子目录. 但不能作用于当前目录的上一级目录. //可以通过 setPath 方法来设置 Cookie 的作用范围, 其中 / 代表站点的根目录. %> <a href="../cookie2.jsp">To Read Cookie</a> <br><br> <%= request.getContextPath() %> </body> </html>
1.5 小结
向浏览器写入Cookie
从浏览器读取Cookie
二、Session
2.1 概述
session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决方案。有时候Session也用来指这种解决方案的存储结构。
Session机制
session机制采用的是在服务器端保持 HTTP 状态信息的方案 。服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否包含了一个session标识(即sessionId),如果已经包含一个sessionId则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含sessionId,则为此客户创建一个session并且生成一个与此session相关联的sessionId,这个session id将在本次响应中返回给客户端保存。
Web服务器中有一些Session对象了
浏览器初次发送一个请求,服务器将新建一个Session对象
服务器返回响应,响应中会在响应头中添加Set-Cookie项,里面有Cookie,包含Session ID
浏览器再请求会携带该Cookie,从而与Session保持连接
session.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="true"%> <!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>Insert title here</title> </head> <body> <%= session.getId() %> <% Cookie cookie = new Cookie("JSESSIONID", session.getId()); cookie.setMaxAge(20); response.addCookie(cookie); %> </body> </html>
保存Session ID的方式
- 保存session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。
- 由于cookie可以被人为的禁用,必须有其它的机制以便在cookie被禁用时仍然能够把session id传递回服务器,经常采用的一种技术叫做URL重写,就是把session id附加在URL路径的后面,附加的方式也有两种,一种是作为URL路径的附加信息,另一种是作为查询字符串附加在URL后面。网络在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。
Session Cookie
session通过SessionID来区分不同的客户,session是以cookie或URL重写为基础的,默认使用cookie来实现,系统会创造一个名为JSESSIONID的输出cookie,这称之为session cookie,以区别persistent cookies(也就是我们通常所说的cookie),session cookie是存储于浏览器内存中的,并不是写到硬盘上的,通常看不到JSESSIONID,但是当把浏览器的cookie禁止后,web服务器会采用URL重写的方式传递Sessionid,这时地址栏看到。
session cookie针对某一次会话而言,会话结束session cookie也就随着消失了,而persistent cookie只是存在于客户端硬盘上的一段文本。关闭浏览器,只会是浏览器端内存里的session cookie消失,但不会使保存在服务器端的session对象消失,同样也不会使已经保存到硬盘上的持久化cookie消失。
2.2 HttpSession生命周期
创建
什么时候创建HttpSession对象
对于JSP:并非浏览器访问服务端的任何一个JSP,服务器都会立即创建一个HttpSession对象
- 若当前的JSP是客户端访问的当前Web应用的第一个资源,且JSP的page指定的session属性值为false,则服务器就不会为JSP创建一个HttpSession对象;
- 若当前JSP不是客户端的当前Web应用的第一个资源,且其他页面已经被创建一个HttpSession对象,则服务器也不会为当前JSP创建一个HttpSession对象,而会把当前会话关联的那个HttpSession对象返回给当前的JSP页面。
对于Servlet:若Servlet是客户端访问的第一个Web应用的资源,则只有调用了request.getSession()或request.getSession(true)才会创建HttpSession对象
page指令的session="false"表示当前JSP页面禁用session隐含变量,但可以心事的使用HttpSession对象
Servlet中获取HttpSession对象
- request.getSession(boolean create):create为false,若没有和当前JSP页面关联的HttpSession对象,则返回null;若有,则返回true。create为true一定会返回一个HttpSession对象,若没有和当前JSP页面关联的HttpSession对象,则服务器创建一个新的HttpSession对象返回,若有,直接返回关联的
- request.getSession():等同于request.getSession(true)
销毁
session在下列情况下被删除:
A.程序调用HttpSession.invalidate()
B.距离上一次收到客户端发送的session id时间间隔超过了session的最大有效时间
- 返回最大时效: getMaxInactiveInterval() 单位是秒
- 设置最大时效: setMaxInactiveInterval(int interval)
- 可以在 web.xml 文件中配置 Session 的最大时效, 单位是分钟.
<session-config> <session-timeout>30</session-timeout> </session-config>
C.服务器进程被停止
注意:关闭浏览器只会使存储在客户端浏览器内存中的session cookie失效,不会使服务器端的session对象失效。
2.3 示例
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>Insert title here</title> </head> <body> SessionID: <%= session.getId() %> <br><br> IsNew: <%= session.isNew() %> <br><br> MaxInactiveInterval: <%= session.getMaxInactiveInterval() %> <br><br> CreateTime: <%= session.getCreationTime() %> <br><br> LastAccessTime: <%= session.getLastAccessedTime() %> <br><br> <% Object username = session.getAttribute("username"); if(username == null){ username = ""; } %> <form action="<%= response.encodeURL("hello.jsp") %>" method="post"> username: <input type="text" name="username" value="<%= username %>"/> <input type="submit" value="Submit"/> </form> </body> </html>
hello.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>Insert title here</title> </head> <body> SessionID: <%= session.getId() %> <br><br> IsNew: <%= session.isNew() %> <br><br> MaxInactiveInterval: <%= session.getMaxInactiveInterval() %> <br><br> CreateTime: <%= session.getCreationTime() %> <br><br> LastAccessTime: <%= session.getLastAccessedTime() %> <br><br> Hello: <%= request.getParameter("username") %> <br><br> <% session.setAttribute("username", request.getParameter("username")); %> <a href="<%= response.encodeURL("login.jsp") %>">重新登录</a> <a href="<%= response.encodeURL("logout.jsp") %>">注销</a> </body> </html>
logout.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>Insert title here</title> </head> <body> SessionID: <%= session.getId() %> <br><br> IsNew: <%= session.isNew() %> <br><br> MaxInactiveInterval: <%= session.getMaxInactiveInterval() %> <br><br> CreateTime: <%= session.getCreationTime() %> <br><br> LastAccessTime: <%= session.getLastAccessedTime() %> <br><br> Bye: <%= session.getAttribute("username") %> <br><br> <a href="login.jsp">重新登录</a> <% session.invalidate(); %> </body> </html>
2.4 URL重写
Servlet规范中引入了一种补充的会话管理机制,它允许不支持Cookie的浏览器也可以与WEB服务器保持连续的会话。这种补充机制要求在响应消息的实体内容中必须包含下一次请求的超链接,并将会话标识号作为超链接的URL地址的一个特殊参数。
将会话标识号以参数形式附加在超链接的URL地址后面的技术称为URL重写。如果在浏览器不支持Cookie或者关闭了Cookie功能的情况下,WEB服务器还要能够与浏览器实现有状态的会话,就必须对所有可能被客户端访问的请求路径(包括超链接、form表单的action属性设置和重定向的URL)进行URL重写。
HttpServletResponse接口中定义了两个用于完成URL重写方法:
- encodeURL方法
- encodeRedirectURL方法
2.5 applictation和session域范围的属性
application域范围的属性
Session域范围的属性
2.6 经典案例
使用Session实现简易购物车
创建一个简单的购物车模型,由三个 jsp 和两个 Servlet 组成:
step-1.jsp
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h4>Step1: 选择要购买的图书:</h4> <form action="<%= request.getContextPath() %>/processStep1" method="post"> <table border="1" cellpadding="10" cellspacing="0"> <tr> <td>书名</td> <td>购买</td> </tr> <tr> <td>Java</td> <td><input type="checkbox" name="book" value="Java"/></td> </tr> <tr> <td>Oracle</td> <td><input type="checkbox" name="book" value="Oracle"/></td> </tr> <tr> <td>Struts</td> <td><input type="checkbox" name="book" value="Struts"/></td> </tr> <tr> <td colspan="2"> <input type="submit" value="Submit"/> </td> </tr> </table> </form> </body> </html>
ProcessStep1Servlet
package com.atguigu.javaweb; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ProcessStep1Servlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //1. 获取选中的图书的信息 String [] books = request.getParameterValues("book"); //2. 把图书信息放入到 HttpSession 中 request.getSession().setAttribute("books", books); //2. 重定向页面到 shoppingcart/step-2.jsp System.out.println(request.getContextPath() + "/shoppingcart/step-2.jsp"); response.sendRedirect(request.getContextPath() + "/shoppingcart/step-2.jsp"); } }
step-2.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>Insert title here</title> </head> <body> <h4>Step2: 请输入寄送地址和信用卡信息</h4> <form action="<%= request.getContextPath() %>/processStep2" method="post"> <table cellpadding="10" cellspacing="0" border="1"> <tr> <td colspan="2">寄送信息</td> </tr> <tr> <td>姓名:</td> <td><input type="text" name="name"/></td> </tr> <tr> <td>寄送地址:</td> <td><input type="text" name="address"/></td> </tr> <tr> <td colspan="2">信用卡信息</td> </tr> <tr> <td>种类:</td> <td> <input type="radio" name="cardType" value="Visa"/>Visa <input type="radio" name="cardType" value="Master"/>Master </td> </tr> <tr> <td>卡号:</td> <td> <input type="text" name="card"/> </td> </tr> <tr> <td colspan="2"><input type="submit" value="Submit"/></td> </tr> </table> </form> </body> </html>
ProcessStep2Servlet
package com.atguigu.javaweb; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class ProcessStep2Servlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //1. 获取请求参数: name, address, cardType, card String name = request.getParameter("name"); String address = request.getParameter("address"); String cardType = request.getParameter("cardType"); String card = request.getParameter("card"); Customer customer = new Customer(name, address, cardType, card); //2. 把请求信息存入到 HttpSession 中 HttpSession session = request.getSession(); session.setAttribute("customer", customer); //3. 重定向页面到 confirm.jsp response.sendRedirect(request.getContextPath() + "/shoppingcart/confirm.jsp"); } }
Customer
package com.atguigu.javaweb; public class Customer { private String name; private String address; private String cardType; private String card; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getCardType() { return cardType; } public void setCardType(String cardType) { this.cardType = cardType; } public String getCard() { return card; } public void setCard(String card) { this.card = card; } public Customer(String name, String address, String cardType, String card) { super(); this.name = name; this.address = address; this.cardType = cardType; this.card = card; } public Customer() { // TODO Auto-generated constructor stub } }
confirm.jsp
<%@page import="com.atguigu.javaweb.Customer"%> <%@ 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>Insert title here</title> </head> <body> <% Customer customer = (Customer)session.getAttribute("customer"); String [] books = (String[])session.getAttribute("books"); %> <table> <tr> <td>顾客姓名:</td> <td><%= customer.getName() %></td> </tr> <tr> <td>地址:</td> <td><%= customer.getAddress() %></td> </tr> <tr> <td>卡号:</td> <td><%= customer.getCard() %></td> </tr> <tr> <td>卡的类型:</td> <td><%= customer.getCardType() %></td> </tr> <tr> <td>Books:</td> <td> <% for(String book: books){ out.print(book); out.print("<br>"); } %> </td> </tr> </table> </body> </html>
使用绝对路径:使用相对路径可能会有问题, 但使用绝对路径肯定没有问题
1). 绝对路径: 相对于当前 WEB 应用的路径. 在当前 WEB 应用的所有的路径前都添加 contextPath 即可.
2). / 什么时候代表站点的根目录, 什么时候代表当前 WEB 应用的根目录
若 / 需要服务器进行内部解析, 则代表的就是 WEB 应用的根目录. 若是交给浏览器了, 则 / 代表的就是站点的根目录
若 / 代表的是 WEB 应用的根目录, 就不需要加上 contextPath 了.
/ 代表什么?
request.getContextPath()可以得到 /Web应用的名称
1)若/交由Servlet容器处理,代表Web应用的根路径,浏览器地址是http://localhost:8080/contextPath/
- 请求转发时:request.getRequestDispatcher("/path/b.jsp").forward(request, response);
- web.xml文件中映射Servlet访问地址
<servlet-mapping> <servlet-name>TestServlet</servlet-name> <url-pattern>/TestServlet</urlpattern> </servlet-mapping>2)浏览器认为是Web站点的根路径,浏览器地址是是http://localhost:8080/
- 超链接
- 表达式中action
- 请求重定向:request.sendRedirect("/a.jsp")
利用Session防止表单重复提交
调用 RequestDispatcher.forward() 方法,浏览器所保留的URL 是先前的表单提交的 URL,此时点击”刷新”, 浏览器将再次提交用户先前输入的数据,引起重复提交
如果采用 HttpServletResponse.sendRedirct() 方法将客户端重定向到成功页面,将不会出现重复一条问题
JS客户端防止表单重复提交
不足:但用户单击”刷新”,或单击”后退”再次提交表单,将导致表单重复提交
利用Session防止表单重复提交
1)重复提交的情况
①在表单提交到一个 Servlet,而 Servlet 又通过请求转发的方式响应一个 JSP(HTML) 页面 ,此时地址栏还保留着 Serlvet 的那个路径,在响应页面点击 "刷新"
②在响应页面没有到达时重复点击 "提交按钮"
③点击 "返回", 再点击 "提交"
2)不是重复提交的情况:点击 "返回", "刷新" 原表单页面, 再 "提交"。
3).如何避免表单的重复提交:在表单中做一个标记, 提交到 Servlet 时, 检查标记是否存在且是否和预定义的标记一致, 若一致, 则受理请求,
并销毁标记, 若不一致或没有标记, 则直接响应提示信息: "重复提交"
①仅提供一个隐藏域: <input type="hidden" name="token" value="atguigu"/>. 行不通:没有方法清除固定的请求参数.
②把标记放在 request 中. 行不通, 因为表单页面刷新后, request 已经被销毁, 再提交表单是一个新的 request.
③把标记放在 session 中. 可以!
> 在原表单页面, 生成一个随机值 token
> 在原表单页面, 把 token 值放入 session 属性中
> 在原表单页面, 把 token 值放入到 隐藏域 中.
> 在目标的 Servlet 中:获取 session 和 隐藏域 中的 token 值
> 比较两个值是否一致:若一致, 受理请求, 且把 session 域中的 token 属性清除
> 若不一致, 则直接响应提示页面:"重复提交"
index.jsp
包含有FORM表单的页面必须通过一个服务器程序动态产生,服务器程序为每次产生的页面中的FORM表单都分配一个唯一的随机标识号,并在FORM表单的一个隐藏字段中设置这个标识号,同时在当前用户的Session域中保存这个标识号。
当用户提交FORM表单时,负责接收这一请求的服务器程序比较FORM表单隐藏字段中的标识号与存储在当前用户的Session域中的标识号是否相同,如果相同则处理表单数据,处理完后清除当前用户的Session域中存储的标识号。在下列情况下,服务器程序将忽略提交的表单请求:
- 当前用户的Session中不存在表单标识号
- 用户提交的表单数据中没有标识号字段
- 存储在当前用户的Session域中的表单标识号与表单数据中的标识号不同
<%@page import="com.atguigu.javaweb.TokenProcessor"%> <%@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>Insert title here</title> </head> <body> <%-- String tokenValue = new Date().getTime() + ""; session.setAttribute("token", tokenValue); --%> <form action="<%= request.getContextPath() %>/tokenServlet" method="post"> <input type="hidden" name="COM.ATGUIGU.TOKEN_KEY" value="<%= TokenProcessor.getInstance().saveToken(request) %>"/> name: <input type="text" name="name"/> <input type="submit" value="Submit"/> </form> </body> </html>
浏览器只有重新向WEB服务器请求包含FORM表单的页面时,服务器程序才又产生另外一个随机标识号,并将这个标识号保存在Session域中和作为新返回的FORM表单中的隐藏字段值。
success.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>Insert title here</title>
</head>
<body>
<h4>Success Page</h4>
</body>
</html>
token.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>Insert title here</title>
</head>
<body>
<h4>对不起, 已经提交过了!</h4>
</body>
</html>
TokenServlet
package com.atguigu.javaweb; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class TokenServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // HttpSession session = request.getSession(); // Object token = session.getAttribute("token"); // String tokenValue = request.getParameter("token"); // System.out.println(token); // System.out.println(tokenValue); // // if(token != null && token.equals(tokenValue)){ // session.removeAttribute("token"); // }else{ // response.sendRedirect(request.getContextPath() + "/token/token.jsp"); // return; // } boolean valid = TokenProcessor.getInstance().isTokenValid(request); if(valid){ TokenProcessor.getInstance().resetToken(request); }else{ response.sendRedirect(request.getContextPath() + "/token/token.jsp"); return; } String name = request.getParameter("name"); //访问数据库服务器... System.out.println("name: " + name); //request.getRequestDispatcher("/token/success.jsp").forward(request, response); response.sendRedirect(request.getContextPath() + "/token/success.jsp"); } }
TokenProcessor
用于管理表单标识号的工具类,它主要用于产生、比较和清除存储在当前用户Session中的表单标识号。为了保证表单标识号的唯一性,每次将当前SessionID和系统时间的组合值按MD5算法计算的结果作为表单标识号,并且将TokenProcessor类设计为单件类
package com.atguigu.javaweb; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class TokenProcessor { private static final String TOKEN_KEY = "COM.ATGUIGU.TOKEN_KEY"; private static final String TRANSACTION_TOKEN_KEY = "TRANSACTION_TOKEN_KEY"; private static TokenProcessor instance = new TokenProcessor(); private long previous; protected TokenProcessor() { super(); } public static TokenProcessor getInstance() { return instance; } public synchronized boolean isTokenValid(HttpServletRequest request) { return this.isTokenValid(request, false); } public synchronized boolean isTokenValid(HttpServletRequest request, boolean reset) { HttpSession session = request.getSession(false); if (session == null) { return false; } String saved = (String) session.getAttribute(TRANSACTION_TOKEN_KEY); if (saved == null) { return false; } if (reset) { this.resetToken(request); } String token = request.getParameter(TOKEN_KEY); if (token == null) { return false; } return saved.equals(token); } public synchronized void resetToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return; } session.removeAttribute(TRANSACTION_TOKEN_KEY); } public synchronized String saveToken(HttpServletRequest request) { HttpSession session = request.getSession(); String token = generateToken(request); if (token != null) { session.setAttribute(TRANSACTION_TOKEN_KEY, token); } return token; } public synchronized String generateToken(HttpServletRequest request) { HttpSession session = request.getSession(); return generateToken(session.getId()); } public synchronized String generateToken(String id) { try { long current = System.currentTimeMillis(); if (current == previous) { current++; } previous = current; byte[] now = new Long(current).toString().getBytes(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(id.getBytes()); md.update(now); return toHex(md.digest()); } catch (NoSuchAlgorithmException e) { return null; } } private String toHex(byte[] buffer) { StringBuffer sb = new StringBuffer(buffer.length * 2); for (int i = 0; i < buffer.length; i++) { sb.append(Character.forDigit((buffer[i] & 0xf0) >> 4, 16)); sb.append(Character.forDigit(buffer[i] & 0x0f, 16)); } return sb.toString(); } }
问题:
同一个用户打开同一个浏览器进程的多个窗口来并发访问同一个WEB站点的多个FORM表单页面时,将会出现表单无法正常提交的情况。
解决方案:
- 将FORM表单的标识号作为表单隐藏字段的名称,如下所示:
-
<input type='hidden' name='4b15c6b2f573831b4b5107d849fcafb8' value=''>
-
- 将所有的表单标识号存储进一个Vector集合对象中,并将Vector集合对象存储进Session域中。当表单提交时,先从Session域中取出Vector集合对象,然后再从Vector集合对象中逐一取出每个表单标识号作为参数调用HttpServletRequest.getParameter方法,如果其中有一次调用的返回值不为null,则接受并处理该表单数据,处理完后将该表单标识号从Vector集合对象中删除。
利用Session实现一次性验证码
一次性验证码的主要目的就是为了限制人们利用工具软件来暴力猜测密码,其原理与利用Session防止表单重复提交的原理基本一样,只是将表单标识号变成了验证码的形式,并且要求用户将提示的验证码手工填写进一个表单字段中,而不是通过表单的隐藏字段自动回传给服务器。
服务器程序接收到表单数据后,首先判断用户是否填写了正确的验证码,只有该验证码与服务器端保存的验证码匹配时,服务器程序才开始正常的表单处理流程。
密码猜测工具要逐一尝试每个密码的前题条件是先输入正确的验证码,而验证码是一次性有效的,这样基本上就阻断了密码猜测工具的自动地处理过程。
基本原理
和表单重复提交一致:
> 在原表单页面, 生成一个验证码的图片, 生成图片的同时, 需要把该图片中的字符串放入到 session 中.
> 在原表单页面, 定义一个文本域, 用于输入验证码.
> 在目标的 Servlet 中: 获取 session 和 表单域 中的 验证码的 值
> 比较两个值是否一致: 若一致, 受理请求, 且把 session 域中的 验证码 属性清除
> 若不一致, 则直接通过重定向的方式返回原表单页面, 并提示用户 "验证码错误"
index.jsp
<%@page import="com.atguigu.javaweb.TokenProcessor"%> <%@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>Insert title here</title> </head> <body> <font color="red"> <%= session.getAttribute("message") == null ? "" : session.getAttribute("message")%> </font> <form action="<%= request.getContextPath() %>/checkCodeServlet" method="post"> name: <input type="text" name="name"/> checkCode: <input type="text" name="CHECK_CODE_PARAM_NAME"/> <img alt="" src="<%= request.getContextPath() %>/validateColorServlet"> <input type="submit" value="Submit"/> </form> </body> </html>
用到的生成验证码的Servlet
package com.atguigu.javaweb; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Random; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ValidateColorServlet extends HttpServlet { public static final String CHECK_CODE_KEY = "CHECK_CODE_KEY"; private static final long serialVersionUID = 1L; //设置验证图片的宽度, 高度, 验证码的个数 private int width = 152; private int height = 40; private int codeCount = 6; //验证码字体的高度 private int fontHeight = 4; //验证码中的单个字符基线. 即:验证码中的单个字符位于验证码图形左上角的 (codeX, codeY) 位置处 private int codeX = 0; private int codeY = 0; //验证码由哪些字符组成 char [] codeSequence = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789".toCharArray(); //初始化验证码图形属性 public void init(){ fontHeight = height - 2; codeX = width / (codeCount + 2); codeY = height - 4; } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //定义一个类型为 BufferedImage.TYPE_INT_BGR 类型的图像缓存 BufferedImage buffImg = null; buffImg = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); //在 buffImg 中创建一个 Graphics2D 图像 Graphics2D graphics = null; graphics = buffImg.createGraphics(); //设置一个颜色, 使 Graphics2D 对象的后续图形使用这个颜色 graphics.setColor(Color.WHITE); //填充一个指定的矩形: x - 要填充矩形的 x 坐标; y - 要填充矩形的 y 坐标; width - 要填充矩形的宽度; height - 要填充矩形的高度 graphics.fillRect(0, 0, width, height); //创建一个 Font 对象: name - 字体名称; style - Font 的样式常量; size - Font 的点大小 Font font = null; font = new Font("", Font.BOLD, fontHeight); //使 Graphics2D 对象的后续图形使用此字体 graphics.setFont(font); graphics.setColor(Color.BLACK); //绘制指定矩形的边框, 绘制出的矩形将比构件宽一个也高一个像素 graphics.drawRect(0, 0, width - 1, height - 1); //随机产生 15 条干扰线, 使图像中的认证码不易被其它程序探测到 Random random = null; random = new Random(); graphics.setColor(Color.GREEN); for(int i = 0; i < 55; i++){ int x = random.nextInt(width); int y = random.nextInt(height); int x1 = random.nextInt(20); int y1 = random.nextInt(20); graphics.drawLine(x, y, x + x1, y + y1); } //创建 randomCode 对象, 用于保存随机产生的验证码, 以便用户登录后进行验证 StringBuffer randomCode; randomCode = new StringBuffer(); for(int i = 0; i < codeCount; i++){ //得到随机产生的验证码数字 String strRand = null; strRand = String.valueOf(codeSequence[random.nextInt(36)]); //把正在产生的随机字符放入到 StringBuffer 中 randomCode.append(strRand); //用随机产生的颜色将验证码绘制到图像中 graphics.setColor(Color.BLUE); graphics.drawString(strRand, (i + 1)* codeX, codeY); } //再把存放有所有随机字符的 StringBuffer 对应的字符串放入到 HttpSession 中 request.getSession().setAttribute(CHECK_CODE_KEY, randomCode.toString()); //禁止图像缓存 response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); //将图像输出到输出流中 ServletOutputStream sos = null; sos = response.getOutputStream(); ImageIO.write(buffImg, "jpeg", sos); sos.close(); } }
package com.atguigu.javaweb; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class CheckCodeServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //1. 获取请求参数: CHECK_CODE_PARAM_NAME String paramCode = request.getParameter("CHECK_CODE_PARAM_NAME"); //2. 获取 session 中的 CHECK_CODE_KEY 属性值 String sessionCode = (String)request.getSession().getAttribute("CHECK_CODE_KEY"); System.out.println(paramCode); System.out.println(sessionCode); //3. 比对. 看是否一致, 若一致说明验证码正确, 若不一致, 说明验证码错误 if(!(paramCode != null && paramCode.equals(sessionCode))){ request.getSession().setAttribute("message", "验证码不一致!"); response.sendRedirect(request.getContextPath() + "/check/index.jsp"); return; } System.out.println("受理请求!"); } }