目录
一. 会话
二. Cookie
三. Session
一. 会话
1.1 会话简介
会话:指客户端(浏览器)和服务端之间的数据传输。客户端与服务器通信过程中,会产生一些数据。会话可简单理解为,用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话。
有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学曾经来过,这称之为有状态会话。
会话管理:就是管理浏览器客户端和服务端之间会话过程产生的会话数据。每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,程序要想办法为每个用户保存这些数据。
比如,A和B分别登陆了某宝购物网站,A买了一个Android手机,B买了一个iPhone手机,当结账时,web服务器需要分别对用户A和B的信息分别保存。根据Java Web之Servlet技术中所说,HttpServletRequest对象和ServletContext对象都可以保存数据,但是这二者在该情形下是不适合使用的。客户端的每次请求,服务器都会产生一个HttpServletRequest对象,该对象只保存本次请求所传递的数据。由于购买和结账是两个不同的请求,所以使用HttpServletRequest对象保存信息会造成丢失。同一个Web应用共享一个ServletContext对象,所以当多个用户结账时无法区分哪个商品是哪个用户购买的,这显然是不可行的。
1.2 保存会话数据的两种技术
1.2.1 Cookie
Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。服务器向客户端发送Cookie时,会在HTTP响应头字段增加Set-Cookie字段,该字段设置的Cookie遵循一定规则,比如以键值对形式保存,Cookie属性值可以有多个,但是这些属性之间必须以分号和空格分隔。
1.2.2 Session
Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在各自的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。
Cookie技术可以将用户信息保存在浏览器中,并且可在多次请求中共享数据,但是如果传递的信息比较多, 使用Cookie技术明显加大服务端程序的处理难度。此时,可以使用Session技术来实现,其通过将会话数据保存在服务端。注意:Session对象对应着一个ID,所以一般是需要客户端记录该对象的ID,通常情况下,Session是通过Cookie技术来传递Session对象ID的。
二. Cookie
2.1 Java中的javax.servlet.http.Cookie类用于创建一个Cookie
为了封装Cookie信息,Servlet API中提供了一个javax.servlet.http.Cookie类,该类包含了生成Cookie信息和提取Cookie信息各个属性的方法,Cookie拥有唯一的构造方法。
Cookie类的主要方法 | |||
No. | 方法 | 类型 | 描述 |
1 | 构造方法 | 实例化Cookie对象,传入cooke名称和cookie的值。Cookie的构造方法唯一 | |
2 | public String getName() | 普通方法 | 取得Cookie的名字 |
3 | public String getValue() | 普通方法 | 取得Cookie的值 |
4 | public void setValue(String newValue) | 普通方法 | 设置Cookie的值 |
5 | public void setMaxAge(int expiry) | 普通方法 | 设置Cookie的最大保存时间,即cookie的有效期,当服务器给浏览器回送一个cookie时,如果在服务器端没有调用setMaxAge方法设置cookie的有效期,那么cookie的有效期只在一次会话过程中有效,用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一次会话,当用户关闭浏览器,会话就结束了,此时cookie就会失效,如果在服务器端使用setMaxAge方法设置了cookie的有效期,比如设置了30分钟,那么当服务器把cookie发送给浏览器时,此时cookie就会在客户端的硬盘上存储30分钟,在30分钟内,即使浏览器关了,cookie依然存在,在30分钟内,打开浏览器访问服务器时,浏览器都会把cookie一起带上,这样就可以在服务器端获取到客户端浏览器传递过来的cookie里面的信息了,这就是cookie设置maxAge和不设置maxAge的区别,不设置maxAge,那么cookie就只在一次会话中有效,一旦用户关闭了浏览器,那么cookie就没有了,那么浏览器是怎么做到这一点的呢,我们启动一个浏览器,就相当于启动一个应用程序,而服务器回送的cookie首先是存在浏览器的缓存中的,当浏览器关闭时,浏览器的缓存自然就没有了,所以存储在缓存中的cookie自然就被清掉了,而如果设置了cookie的有效期,那么浏览器在关闭时,就会把缓存中的cookie写到硬盘上存储起来,这样cookie就能够一直存在了。 |
6 | public int getMaxAge() | 普通方法 | 获取Cookies的有效期。返回值为整数秒 |
7 | public void setPath(String uri) | 普通方法 | 设置cookie的有效路径,比如把cookie的有效路径设置为"/xdp",那么浏览器访问"xdp"目录下的web资源时,都会带上cookie,再比如把cookie的有效路径设置为"/xdp/gacl",那么浏览器只有在访问"xdp"目录下的"gacl"这个目录里面的web资源时才会带上cookie一起访问,而当访问"xdp"目录下的web资源时,浏览器是不带cookie的 |
8 | public String getPath() | 普通方法 | 获取cookie的有效路径 |
9 | public void setDomain(String pattern) | 普通方法 | 有效域 |
10 | public String getDomain() | 普通方法 | 获取cookie的有效域 |
request接口中也定义了一个getCookie()方法,它用于获取客户端提交的Cookie;同样,response接口中也定义了一个addCookie()方法,它用于在其响应头中增加一个相应的Set-Cookie头字段。
2.2 Cookie的使用范例
2.2.1 使用Cookie 记录上一次访问的时间
1 package gac.xdp.cookie;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.util.Date;
6 import javax.servlet.ServletException;
7 import javax.servlet.http.Cookie;
8 import javax.servlet.http.HttpServlet;
9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletResponse;
11
12 /**
13 * @author gacl
14 * cookie实例:获取用户上一次访问的时间
15 */
16 public class CookieDemo01 extends HttpServlet {
17
18 public void doGet(HttpServletRequest request, HttpServletResponse response)
19 throws ServletException, IOException {
20 //设置服务器端以UTF-8编码进行输出
21 response.setCharacterEncoding("UTF-8");
22 //设置浏览器以UTF-8编码进行接收显示,解决中文乱码问题
23 response.setContentType("text/html;charset=UTF-8");
24 PrintWriter out = response.getWriter();
25 //获取浏览器访问服务器时传递过来的cookie数组
26 Cookie[] cookies = request.getCookies();
27 //如果用户是第一次访问,那么得到的cookies将是null
28 if (cookies!=null) {
29 out.write("您上次访问的时间是:");
30 for (int i = 0; i < cookies.length; i++) {
31 Cookie cookie = cookies[i];
32 if (cookie.getName().equals("lastAccessTime")) {
33 Long lastAccessTime =Long.parseLong(cookie.getValue());
34 Date date = new Date(lastAccessTime);
35 out.write(date.toLocaleString());
36 }
37 }
38 }else {
39 out.write("这是您第一次访问本站!");
40 }
41
42 //用户访问过之后重新设置用户的访问时间,存储到cookie中,然后发送到客户端浏览器
43 Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");//创建一个cookie,cookie的名字是lastAccessTime
44 //将cookie对象添加到response对象中,这样服务器在输出response对象中的内容时就会把cookie也输出到客户端浏览器
45 response.addCookie(cookie);
46 }
47
48 public void doPost(HttpServletRequest request, HttpServletResponse response)
49 throws ServletException, IOException {
50 doGet(request, response);
51 }
52
53 }
2.2.2 为Cookie 设置一个有效期
在上面的例子中,在程序代码中并没有使用setMaxAge()方法设置cookie的有效期,所以当关闭浏览器之后,cookie就失效了,要想在关闭了浏览器之后,cookie依然有效,那么在创建cookie时,就要为cookie设置一个有效期。如下所示 :
//用户访问过之后重新设置用户的访问时间,存储到cookie中,然后发送到客户端浏览器
Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");//创建一个cookie,cookie的名字是lastAccessTime
//设置Cookie的有效期为1天,整数秒
cookie.setMaxAge(24*60*60);
//将cookie对象添加到response对象中,这样服务器在输出response对象中的内容时就会把cookie也输出到客户端浏览器
response.addCookie(cookie);
用户第一次访问时,服务器发送给浏览器的cookie就存储到了硬盘上,如下所示:
这样即使关闭了浏览器,下次再访问时,也依然可以通过cookie获取用户上一次访问的时间。
2.3 Cookie的注意细节
1)一个Cookie只能标识一种信息,它至少含有一个标识该信息的名称(name)和设置值(value)。
2)一个WEB站点可以给一个WEB浏览器发送多个Cookie,一个WEB浏览器也可以存储多个WEB站点提供的Cookie。
3)浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。
4)如果创建了一个cookie,并将他发送到浏览器,默认情况下它是一个会话级别的cookie(即存储在浏览器的内存中),用户退出浏览器之后即被删除。若希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以 秒 为单位的时间。将最大时效设为0则是命令浏览器删除该cookie。
2.3.1 删除Cookie
注意:删除cookie时,path必须一致,否则不会删除
1 package gac.xdp.cookie;
2
3 import java.io.IOException;
4
5 import javax.servlet.ServletException;
6 import javax.servlet.http.Cookie;
7 import javax.servlet.http.HttpServlet;
8 import javax.servlet.http.HttpServletRequest;
9 import javax.servlet.http.HttpServletResponse;
10
11 /**
12 * @author gacl
13 * 删除cookie
14 */
15 public class CookieDemo02 extends HttpServlet {
16
17 public void doGet(HttpServletRequest request, HttpServletResponse response)
18 throws ServletException, IOException {
19 //创建一个名字为lastAccessTime的cookie
20 Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");
21 //将cookie的有效期设置为0,命令浏览器删除该cookie
22 cookie.setMaxAge(0);
23 response.addCookie(cookie);
24 }
25
26 public void doPost(HttpServletRequest request, HttpServletResponse response)
27 throws ServletException, IOException {
28 doGet(request, response);
29 }
30 }
2.3.2 Cookie里面存取中文
1)第一步,encode() 转码
要想在cookie中存储中文,那么必须使用URLEncoder类里面的encode(String s, String enc)方法进行中文转码,例如:
Cookie cookie = new Cookie("userName", URLEncoder.encode("孤傲苍狼", "UTF-8"));
response.addCookie(cookie);
2)第二步,decode() 解码
在获取cookie中的中文数据时,再使用URLDecoder类里面的decode(String s, String enc)方法进行解码,例如:
URLDecoder.decode(cookies[i].getValue(), "UTF-8")
三. Session
3.1 Session简介
在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。
3.2 Session和Cookie的主要区别
Cookie是把用户的数据写给用户的浏览器。
Session技术把用户的数据写到用户独占的session中。
Session对象由服务器创建,开发人员可以调用request对象的getSession()方法得到session对象。
3.3 session实现原理
服务器是如何实现一个session为一个用户浏览器服务的?
服务器创建session出来后,会把session的id号,以cookie的形式回写给客户机,这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,就会使用内存中与之对应的session为之服务。可以用如下的代码证明:
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import javax.servlet.ServletException;
5 import javax.servlet.http.HttpServlet;
6 import javax.servlet.http.HttpServletRequest;
7 import javax.servlet.http.HttpServletResponse;
8 import javax.servlet.http.HttpSession;
9
10 public class SessionDemo1 extends HttpServlet {
11
12 public void doGet(HttpServletRequest request, HttpServletResponse response)
13 throws ServletException, IOException {
14
15 response.setCharacterEncoding("UTF=8");
16 response.setContentType("text/html;charset=UTF-8");
17 //使用request对象的getSession()获取session,如果session不存在则创建一个
18 HttpSession session = request.getSession();
19 //将数据存储到session中
20 session.setAttribute("data", "孤傲苍狼");
21 //获取session的Id
22 String sessionId = session.getId();
23 //判断session是不是新创建的
24 if (session.isNew()) {
25 response.getWriter().print("session创建成功,session的id是:"+sessionId);
26 }else {
27 response.getWriter().print("服务器已经存在该session了,session的id是:"+sessionId);
28 }
29 }
30
31 public void doPost(HttpServletRequest request, HttpServletResponse response)
32 throws ServletException, IOException {
33 doGet(request, response);
34 }
35 }
第一次访问时,服务器会创建一个新的sesion,并且把session的Id以cookie的形式发送给客户端浏览器,如下图所示:
点击刷新按钮,再次请求服务器,此时就可以看到浏览器再请求服务器时,会把存储到cookie中的session的Id一起传递到服务器端了,如下图所示:
我猜想request.getSession()方法内部新创建了Session之后一定是做了如下的处理
1 //获取session的Id
2 String sessionId = session.getId();
3 //将session的Id存储到名字为JSESSIONID的cookie中
4 Cookie cookie = new Cookie("JSESSIONID", sessionId);
5 //设置cookie的有效路径
6 cookie.setPath(request.getContextPath());
7 response.addCookie(cookie);
3.4 浏览器禁用Cookie后的session处理
3.4.1 IE8禁用Cookie
工具->internet选项->隐私->设置->将滑轴拉到最顶上(阻止所有cookies)
3.4.2 通过重写URL 解决IE8禁用Cookie
response.encodeRedirectURL(java.lang.String url) 用于对sendRedirect()方法后的url地址进行重写。
response.encodeURL(java.lang.String url)用于对表单action和超链接的url地址进行重写
3.4.3 禁用Cookie后servlet共享Session中的数据
先给出一个具体的案例,代码如下:
IndexServlet
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.util.LinkedHashMap;
6 import java.util.Map;
7 import java.util.Set;
8 import javax.servlet.ServletException;
9 import javax.servlet.http.HttpServlet;
10 import javax.servlet.http.HttpServletRequest;
11 import javax.servlet.http.HttpServletResponse;
12
13 //首页:列出所有书
14 public class IndexServlet extends HttpServlet {
15
16 public void doGet(HttpServletRequest request, HttpServletResponse response)
17 throws ServletException, IOException {
18
19 response.setContentType("text/html;charset=UTF-8");
20 PrintWriter out = response.getWriter();
21 //创建Session
22 request.getSession();
23 out.write("本网站有如下书:<br/>");
24 Set<Map.Entry<String,Book>> set = DB.getAll().entrySet();
25 for(Map.Entry<String,Book> me : set){
26 Book book = me.getValue();
27 String url =request.getContextPath()+ "/servlet/BuyServlet?id=" + book.getId();
28 //response. encodeURL(java.lang.String url)用于对表单action和超链接的url地址进行重写
29 url = response.encodeURL(url);//将超链接的url地址进行重写
30 out.println(book.getName() + " <a href='"+url+"'>购买</a><br/>");
31 }
32 }
33
34 public void doPost(HttpServletRequest request, HttpServletResponse response)
35 throws ServletException, IOException {
36 doGet(request, response);
37 }
38 }
39
40
41 /**
42 * @author gacl
43 * 模拟数据库
44 */
45 class DB{
46 private static Map<String,Book> map = new LinkedHashMap<String,Book>();
47 static{
48 map.put("1", new Book("1","javaweb开发"));
49 map.put("2", new Book("2","spring开发"));
50 map.put("3", new Book("3","hibernate开发"));
51 map.put("4", new Book("4","struts开发"));
52 map.put("5", new Book("5","ajax开发"));
53 }
54
55 public static Map<String,Book> getAll(){
56 return map;
57 }
58 }
59
60 class Book{
61
62 private String id;
63 private String name;
64
65 public Book() {
66 super();
67 }
68 public Book(String id, String name) {
69 super();
70 this.id = id;
71 this.name = name;
72 }
73 public String getId() {
74 return id;
75 }
76 public void setId(String id) {
77 this.id = id;
78 }
79 public String getName() {
80 return name;
81 }
82 public void setName(String name) {
83 this.name = name;
84 }
85 }
BuyServlet
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.List;
6 import javax.servlet.ServletException;
7 import javax.servlet.http.HttpServlet;
8 import javax.servlet.http.HttpServletRequest;
9 import javax.servlet.http.HttpServletResponse;
10 import javax.servlet.http.HttpSession;
11
12 public class BuyServlet extends HttpServlet {
13
14 public void doGet(HttpServletRequest request, HttpServletResponse response)
15 throws ServletException, IOException {
16 String id = request.getParameter("id");
17 Book book = DB.getAll().get(id); //得到用户想买的书
18 HttpSession session = request.getSession();
19 List<Book> list = (List) session.getAttribute("list"); //得到用户用于保存所有书的容器
20 if(list==null){
21 list = new ArrayList<Book>();
22 session.setAttribute("list", list);
23 }
24 list.add(book);
25 //response. encodeRedirectURL(java.lang.String url)用于对sendRedirect方法后的url地址进行重写
26 String url = response.encodeRedirectURL(request.getContextPath()+"/servlet/ListCartServlet");
27 System.out.println(url);
28 response.sendRedirect(url);
29 }
30
31 public void doPost(HttpServletRequest request, HttpServletResponse response)
32 throws ServletException, IOException {
33 doGet(request, response);
34 }
35 }
ListCartServlet
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.util.List;
6 import javax.servlet.ServletException;
7 import javax.servlet.http.HttpServlet;
8 import javax.servlet.http.HttpServletRequest;
9 import javax.servlet.http.HttpServletResponse;
10 import javax.servlet.http.HttpSession;
11
12 public class ListCartServlet extends HttpServlet {
13
14 public void doGet(HttpServletRequest request, HttpServletResponse response)
15 throws ServletException, IOException {
16 response.setContentType("text/html;charset=UTF-8");
17 PrintWriter out = response.getWriter();
18 HttpSession session = request.getSession();
19 List<Book> list = (List) session.getAttribute("list");
20 if(list==null || list.size()==0){
21 out.write("对不起,您还没有购买任何商品!!");
22 return;
23 }
24
25 //显示用户买过的商品
26 out.write("您买过如下商品:<br>");
27 for(Book book : list){
28 out.write(book.getName() + "<br/>");
29 }
30 }
31
32 public void doPost(HttpServletRequest request, HttpServletResponse response)
33 throws ServletException, IOException {
34 doGet(request, response);
35 }
36 }
在禁用了cookie的IE8下的运行效果如下:
通过查看IndexServlet生成的html代码可以看到,每一个超链接后面都带上了session的Id,如下所示
1 本网站有如下书:<br/>javaweb开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=1'>购买</a><br/>
2 spring开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=2'>购买</a><br/>
3 hibernate开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=3'>购买</a><br/>
4 struts开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=4'>购买</a><br/>
5 ajax开发 <a href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2537CDE2DB2?id=5'>购买</a><br/>
所以,当浏览器禁用了cookie后,就可以用URL重写这种解决方案解决Session数据共享问题。而且response. encodeRedirectURL(java.lang.String url) 和response. encodeURL(java.lang.String url)是两个非常智能的方法,当检测到浏览器没有禁用cookie时,那么就不进行URL重写了。我们在没有禁用cookie的火狐浏览器下访问,效果如下:
从演示动画中可以看到,浏览器第一次访问时,服务器创建Session,然后将Session的Id以Cookie的形式发送回给浏览器,response. encodeURL(java.lang.String url)方法也将URL进行了重写,当点击刷新按钮第二次访问,由于火狐浏览器没有禁用cookie,所以第二次访问时带上了cookie,此时服务器就可以知道当前的客户端浏览器并没有禁用cookie,那么就通知response. encodeURL(java.lang.String url)方法不用将URL进行重写了。
3.5 session对象的创建和销毁时机
3.5.1 session对象的创建时机
在程序中第一次调用request.getSession()方法时就会创建一个新的Session,可以用isNew()方法来判断Session是不是新创建的
范例:创建session
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在session,session的id是:"+sessionId);
}
3.5.2 session对象的销毁时机
session对象默认30分钟没有使用,则服务器会自动销毁session,可以在web.xml文件中可以手工配置session的失效时间,单位是分钟,(区别:Cookie的单位是秒)例如:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <web-app version="2.5"
3 xmlns="http://java.sun.com/xml/ns/javaee"
4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
6 http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
7 <display-name></display-name>
8
9 <welcome-file-list>
10 <welcome-file>index.jsp</welcome-file>
11 </welcome-file-list>
12
13 <!-- 设置Session的有效时间:以分钟为单位-->
14 <session-config>
15 <session-timeout>15</session-timeout>
16 </session-config>
17
18 </web-app>
当需要在程序中手动设置Session失效时,可以手工调用session.invalidate()方法,摧毁session。
1 HttpSession session = request.getSession();
2 //手工调用session.invalidate方法,摧毁session
3 session.invalidate();
3.6 使用Session 防止表单重复提交
在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用户可能会以为是自己没有提交表单,就会再点击提交按钮重复地(第二次)提交表单,我们在开发中必须防止表单重复提交。
3.6.1 表单重复提交的场景
比如,有下面的form.jsp页面
1 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
2 <!DOCTYPE HTML>
3 <html>
4 <head>
5 <title>Form表单</title>
6 </head>
7
8 <body>
9 <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
10 用户名:<input type="text" name="username">
11 <input type="submit" value="提交" id="submit">
12 </form>
13 </body>
14 </html>
form表单提交到DoFormServlet进行处理
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import javax.servlet.ServletException;
5 import javax.servlet.http.HttpServlet;
6 import javax.servlet.http.HttpServletRequest;
7 import javax.servlet.http.HttpServletResponse;
8
9 public class DoFormServlet extends HttpServlet {
10
11 public void doGet(HttpServletRequest request, HttpServletResponse response)
12 throws ServletException, IOException {
13 //客户端是以UTF-8编码传输数据到服务器端的,所以需要设置服务器端以UTF-8的编码进行接收,否则对于中文数据就会产生乱码
14 request.setCharacterEncoding("UTF-8");
15 String userName = request.getParameter("username");
16 try {
17 //让当前的线程睡眠3秒钟,模拟网络延迟而导致表单重复提交的现象
18 Thread.sleep(3*1000);
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 System.out.println("向数据库中插入数据:"+userName);
23 }
24
25 public void doPost(HttpServletRequest request, HttpServletResponse response)
26 throws ServletException, IOException {
27 doGet(request, response);
28 }
29 }
如果没有进行form表单重复提交处理,那么在网络延迟的情况下下面的操作将会导致form表单重复提交多次
场景一,在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交
演示动画如下所示:
场景二,表单提交后用户点击【刷新】按钮导致表单重复提交
演示动画如下所示:
点击浏览器的刷新按钮,就是把浏览器上次做的事情再做一次,因为这样也会导致表单重复提交。
场景三,用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交
演示动画如下所示:
3.6.2 利用JavaScript防止表单重复提交
既然存在上述所说的表单重复提交问题,那么我们就要想办法解决,比较常用的方法是采用JavaScript来防止表单重复提交,具体做法如下:
修改form.jsp页面,添加如下的JavaScript代码来防止表单重复提交
1 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
2 <!DOCTYPE HTML>
3 <html>
4 <head>
5 <title>Form表单</title>
6 <script type="text/javascript">
7 var isCommitted = false;//表单是否已经提交标识,默认为false
8 function dosubmit(){
9 if(isCommitted==false){
10 isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true
11 return true;//返回true让表单正常提交
12 }else{
13 return false;//返回false那么表单将不提交
14 }
15 }
16 </script>
17 </head>
18
19 <body>
20 <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" onsubmit="return dosubmit()" method="post">
21 用户名:<input type="text" name="username">
22 <input type="submit" value="提交" id="submit">
23 </form>
24 </body>
25 </html>
我们看看使用了JavaScript来防止表单提交重复是否可以成功,运行效果如下:
可以看到,针对"在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交"这个应用场景,使用JavaScript是可以解决这个问题的,解决的做法就是"用JavaScript控制Form表单只能提交一次"(方法 1)。
除了用这种方式之外,经常见的另一种方式就是表单提交之后,将提交按钮设置为不可用(方法 2),让用户没有机会点击第二次提交按钮,代码如下:
1 function dosubmit(){
2 //获取表单提交按钮
3 var btnSubmit = document.getElementById("submit");
4 //将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
5 btnSubmit.disabled= "disabled";
6 //返回true让表单可以正常提交
7 return true;
8 }
运行效果如下:
另外还有一种做法就是提交表单后,将提交按钮隐藏起来(方法 3),这种做法和将提交按钮设置为不可用是差不多的,个人觉得将提交按钮隐藏影响到页面布局的美观,并且可能会让用户误以为是bug(怎么我一点击按钮,按钮就不见了呢?用户可能会有这样的疑问),我个人在开发中用得比较多的是表单提交后,将提交按钮设置为不可用,反正使用JavaScript防止表单重复提交的做法都是差不多的,目的都是让表单只能提交一次,这样就可以做到表单不重复提交了。
使用JavaScript防止表单重复提交的做法只对上述提交到导致表单重复提交的三种场景中的【场景一】有效,而对于【场景二】和【场景三】是没有用,依然无法解决表单重复提交问题。
3.6.3 利用Session 防止表单重复提交
对于【场景二】和【场景三】导致表单重复提交的问题,既然客户端无法解决,那么就在服务器端解决,在服务器端解决就需要用到session了。
具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。
在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
当前用户的Session中不存在Token(令牌)。
用户提交的表单数据中没有Token(令牌)。
也就是说,当前用户的Session中存在Token,用户提交的表单数据中也存在Token,而且两个Token相同,这时候才会处理用户提交的表单请求。
看具体的范例:
1)创建FormServlet,用于生成Token(令牌)和跳转到form.jsp页面
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import javax.servlet.ServletException;
5 import javax.servlet.http.HttpServlet;
6 import javax.servlet.http.HttpServletRequest;
7 import javax.servlet.http.HttpServletResponse;
8
9 public class FormServlet extends HttpServlet {
10 private static final long serialVersionUID = -884689940866074733L;
11
12 public void doGet(HttpServletRequest request, HttpServletResponse response)
13 throws ServletException, IOException {
14
15 String token = TokenProccessor.getInstance().makeToken();//创建令牌
16 System.out.println("在FormServlet中生成的token:"+token);
17 request.getSession().setAttribute("token", token); //在服务器使用session保存token(令牌)。session.setAttribute()
18 request.getRequestDispatcher("/form.jsp").forward(request, response);//跳转到form.jsp页面
19 }
20
21 public void doPost(HttpServletRequest request, HttpServletResponse response)
22 throws ServletException, IOException {
23 doGet(request, response);
24 }
25 }
2)在form.jsp中使用隐藏域来存储Token(令牌)
1 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
2 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
3 <html>
4 <head>
5 <title>form表单</title>
6 </head>
7
8 <body>
9 <form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
10 <%--使用隐藏域存储生成的token session.getAttribute()--%>
11 <%--
12 <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
13 --%>
14 <%--使用EL表达式取出存储在session中的token--%>
15 <input type="hidden" name="token" value="${token}"/>
16 用户名:<input type="text" name="username">
17 <input type="submit" value="提交">
18 </form>
19 </body>
20 </html>
3)DoFormServlet处理表单提交
1 package xdp.gacl.session;
2
3 import java.io.IOException;
4 import javax.servlet.ServletException;
5 import javax.servlet.http.HttpServlet;
6 import javax.servlet.http.HttpServletRequest;
7 import javax.servlet.http.HttpServletResponse;
8
9 public class DoFormServlet extends HttpServlet {
10
11 public void doGet(HttpServletRequest request, HttpServletResponse response)
12 throws ServletException, IOException {
13
14 boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
15 if(b==true){
16 System.out.println("请不要重复提交");
17 return;
18 }
19 request.getSession().removeAttribute("token");//移除session中的token
20 System.out.println("处理用户提交请求!!");
21 }
22
23 /**
24 * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
25 * @param request
26 * @return
27 * true 用户重复提交了表单
28 * false 用户没有重复提交表单
29 */
30 private boolean isRepeatSubmit(HttpServletRequest request) {
31 String client_token = request.getParameter("token");
32 //1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
33 if(client_token==null){
34 return true;
35 }
36 //取出存储在Session中的token session.getAttribute()
37 String server_token = (String) request.getSession().getAttribute("token");
38 //2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
39 if(server_token==null){
40 return true;
41 }
42 //3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
43 if(!client_token.equals(server_token)){
44 return true;
45 }
46
47 return false;
48 }
49
50 public void doPost(HttpServletRequest request, HttpServletResponse response)
51 throws ServletException, IOException {
52 doGet(request, response);
53 }
54
55 }
生成Token的工具类TokenProccessor
1 package xdp.gacl.session;
2
3 import java.security.MessageDigest;
4 import java.security.NoSuchAlgorithmException;
5 import java.util.Random;
6 import sun.misc.BASE64Encoder;
7
8 public class TokenProccessor {
9
10 /*
11 *单例设计模式(保证类的对象在内存中只有一个)
12 *1、把类的构造函数私有
13 *2、自己创建一个类的对象
14 *3、对外提供一个公共的方法,返回类的对象
15 */
16 private TokenProccessor(){}
17
18 private static final TokenProccessor instance = new TokenProccessor();
19
20 /**
21 * 返回类的对象
22 * @return
23 */
24 public static TokenProccessor getInstance(){
25 return instance;
26 }
27
28 /**
29 * 生成Token
30 * Token:Nv6RRuGEVvmGjB+jimI/gw==
31 * @return
32 */
33 public String makeToken(){ //checkException
34 // 7346734837483 834u938493493849384 43434384
35 String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
36 //数据指纹 128位长 16个字节 md5
37 try {
38 MessageDigest md = MessageDigest.getInstance("md5");
39 byte md5[] = md.digest(token.getBytes());
40 //base64编码--任意二进制编码明文字符 adfsdfsdfsf
41 BASE64Encoder encoder = new BASE64Encoder();
42 return encoder.encode(md5);
43 } catch (NoSuchAlgorithmException e) {
44 throw new RuntimeException(e);
45 }
46 }
47 }
首先访问FormServlet,在FormServlet中生成Token之后再重定向到form.jsp页面,这次是在服务器端处理表单重复提交的,运行效果如下:
从运行效果中可以看到,通过这种方式处理表单重复提交,可以解决上述的场景二和场景三中出现的表单重复提交问题。