利用Session防止表单重复提交
- 重复提交的危害:
- 在投票的网页上不停地提交,实现了刷票的效果。
- 注册多个用户,不断发帖子,扰乱正常发帖秩序。
- 首先我们来看一下常见的重复提交。
- 在处理表单的Servlet中刷新。
- 后退再提交
- 网络延迟,多次点击提交按钮
- 下面的gif是后退再提交,在处理提交请求的Servlet中刷新
下面的gif是网络延迟,多次点击提交按钮
- 对于网络延迟造成的多次提交数据给服务器,其实是客户端的问题。于是,我们可以使用javaScript来防止这种情况
- 要做的事情也非常简单:当用户第一次点击提交按钮时,把数据提交给服务器。当用户再次点击提交按钮时,就不把数据提交给服务器了。
- 监听用户提交事件。只能让用户提交一次表单!
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>表单提交</title> <script type="text/javascript"> //定义一个全局标识量:是否已经提交过表单数据 var isCommitted = false; function doSubmit() { //false表示的是没有提交过,于是就可以让表单提交给Servlet if(isCommitted==false) { isCommitted = true; return true; }else { return false; } } </script> </head> <body> <form action="/ouzicheng/Servlet7" onsubmit="return doSubmit()"> 用户名:<input type="text" name="username"> <input type="submit" value="提交"> </form> </body> </html>
好的,我们来试一下是不是真的可以解决网络延迟所造成的多次提交表单数据,注意鼠标,我已经点击过很多次的了!
- 由于网络延迟造成的多次提交数据给服务器,我们还可以使用javaScript代码这样解决:当我点击过一次提交按钮时,我就把提交的按钮隐藏起来。不能让用户点击了!
- 想要让按钮隐藏起来,也很简单。只要获取到按钮的节点,就可以控制按钮的隐藏或显示了!
<script type="text/javascript"> function doSubmit() { var button = document.getElementById("button"); button.disabled = disabled; return true; } </script>
我们再来看一下效果
- 在处理表单的Servlet中刷新和后退再提交这两种方式不能只靠客户端来限制了。也就是说javaScript代码无法阻止这两种情况的发生。
- 于是乎,我们就想得用其他办法来阻止表单数据重复提交了。我们现在学了Session,Session可以用来标识一个用户是否登陆了。Session的原理也说了:不同的用户浏览器会拥有不同的Session。而request和ServletContext为什么就不行呢?request的域对象只能是一次http请求,提交表单数据的时候request域对象的数据取不出来。ServletContext代表整个web应用,如果有几个用户浏览器同时访问,ServletContext域对象的数据会被多次覆盖掉,也就是说域对象的数据就毫无意义了。
- 可能到这里,我们会想到:在提交数据的时候,存进Session域对象的数据,在处理提交数据的Servlet中判断Session域对象数据????。究竟判断Session什么?判断Session域对象的数据不为null?没用呀,既然已经提交过来了,那肯定不为null。
- 此时,我们就想到了,在表单中还有一个隐藏域,可以通过隐藏域把数据交给服务器。
- 判断Session域对象的数据和jsp隐藏域提交的数据是否对应。
- 判断隐藏域的数据是否为空【如果为空,就是直接访问表单处理页面的Servlet】
- 判断Session的数据是否为空【servlet判断完是否重复提交,最好能立马移除Session的数据,不然还没有移除的时候,客户端那边儿的请求又来了,就又能匹配了,产生了重复提交。如果Session域对象数据为空,证明已经提交过数据了!】
- 我们向Session域对象的存入数据究竟是什么呢?简单的一个数字?好像也行啊。因为只要Session域对象的数据和jsp隐藏域带过去的数据对得上号就行了呀,反正在Servlet上判断完是否重复提交,会立马把Session的数据移除掉的。更专业的做法是:向Session域对象存入的数据是一个随机数【Token--令牌】。
- 生成一个独一无二的随机数
/* * 产生随机数就应该用一个对象来生成,这样可以避免随机数的重复。 * 所以设计成单例 * */ public class TokenProcessor { private TokenProcessor() { } private final static TokenProcessor TOKEN_PROCESSOR = new TokenProcessor(); public static TokenProcessor getInstance() { return TOKEN_PROCESSOR; } public static String makeToken() { //这个随机生成出来的Token的长度是不确定的 String token = String.valueOf(System.currentTimeMillis() + new Random().nextInt(99999999)); try { //我们想要随机数的长度一致,就要获取到数据指纹 MessageDigest messageDigest = MessageDigest.getInstance("md5"); byte[] md5 = messageDigest.digest(token.getBytes()); //如果我们直接 return new String(md5)出去,得到的随机数会乱码。 //因为随机数是任意的01010101010,在转换成字符串的时候,会查gb2312的码表,gb2312码表不一定支持该二进制数据,得到的就是乱码 //于是乎经过base64编码成了明文的数据 BASE64Encoder base64Encoder = new BASE64Encoder(); return base64Encoder.encode(md5); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } }
创建Token随机数,并跳转到jsp页面
//生出随机数 TokenProcessor tokenProcessor = TokenProcessor.getInstance(); String token = tokenProcessor.makeToken(); //将随机数存进Session中 request.getSession().setAttribute("token", token); //跳转到显示页面 request.getRequestDispatcher("/login.jsp").forward(request, response);
jsp隐藏域获取到Session的值
<form action="/ouzicheng/Servlet7" > 用户名:<input type="text" name="username"> <input type="submit" value="提交" id="button"> <%--使用EL表达式取出session中的Token--%> <input type="hidden" name="token" value="${token}" > </form>
在处理表单提交页面中判断:jsp隐藏域是否有值带过来,Session中的值是否为空,Session中的值和jsp隐藏域带过来的值是否相等
String serverValue = (String) request.getSession().getAttribute("token"); String clientValue = request.getParameter("token"); if (serverValue != null && clientValue != null && serverValue.equals(clientValue)) { System.out.println("处理请求"); //清除Session域对象数据 request.getSession().removeAttribute("token"); }else { System.out.println("请不要重复提交数据!"); }
下面我们再来看一下,已经可以解决表单重复提交的问题了!
实现原理是非常简单的:
- 在session域中存储一个token
- 然后前台页面的隐藏域获取得到这个token
- 在第一次访问的时候,我们就判断seesion有没有值,如果有就比对。对比正确后我们就处理请求,接着就把session存储的数据给删除了
- 等到再次访问的时候,我们session就没有值了,就不受理前台的请求了!
一次性校验码
- 一次性校验码其实就是为了防止暴力猜测密码
- 在讲response对象的时候,我们使用response对象输出过验证码,但是没有去验证!
- 验证的原理也非常简单:生成验证码后,把验证码的数据存进Session域对象中,判断用户输入验证码是否和Session域对象的数据一致。
- 生成验证码图片,并将验证码存进Session域中
(80, 20, BufferedImage.TYPE_INT_RGB); //获取到这张图片 Graphics2D graphics2D = (Graphics2D) bufferedImage.getGraphics(); //设置背景色为白色 graphics2D.setColor(Color.white); graphics2D.fillRect(0, 0, 80, 20); //设置图片的字体和颜色 graphics2D.setFont(new Font(null, Font.BOLD, 20)); graphics2D.setColor(Color.BLUE); //生成随机数 String randomNum = makeNum(); //往这张图片上写数据,横坐标是0,纵坐标是20 graphics2D.drawString(randomNum, 0, 20); //将随机数存进Session域中 request.getSession().setAttribute("randomNum", randomNum); //控制浏览器不缓存该图片 response.setHeader("Expires", "-1"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Pragma", "no-cache"); //通知浏览器以图片的方式打开 response.setHeader("Content-type", "image/jpeg"); //把图片写给浏览器 ImageIO.write(bufferedImage, "jpg", response.getOutputStream());
生成随机数的方法:
private String makeNum() { Random random = new Random(); //生成0-6位的随机数 int num = random.nextInt(999999); //验证码的数位全都要6位数,于是将该随机数转换成字符串,不够位数就添加 String randomNum = String.valueOf(num); //使用StringBuffer来拼凑字符串 StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < 6 - randomNum.length(); i++) { stringBuffer.append("0"); } return stringBuffer.append(randomNum).toString(); }
jsp显示页面
<form action="/ouzicheng/Login2Servlet"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> 验证码:<input type="text" name="randomNum"> <img src="/ouzicheng/ImageServlet" ><br><br> <input type="submit" value="提交"> </form>
处理提交表单数据的Servlet,判断用户带过来验证码的数据是否和Session的数据相同。
//获取用户输入验证码的数据 String client_randomNum = request.getParameter("randomNum"); //获取Session中的数据 String session_randomNum = (String) request.getSession().getAttribute("randomNum"); //判断他俩数据是否相等,用户是否有输入验证码,Session中是否为空 if (client_randomNum == null || session_randomNum == null || !client_randomNum.equals(session_randomNum)) { System.out.println("验证码错误了!!!"); return ; } //下面就是验证用户名和密码...................
显示页面是这样子的