1.概念

1.1 会话的概念

会话可简单理解为:用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话。
有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学曾经来过,这称之为有状态会话。

1.2 会话过程中要解决的一些问题?

每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,程序要想办法为每个用户保存这些数据。

1.3 保存会话数据的两种技术

1.Cookie

Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。

2.Session
Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在各自的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。

2.cookie

cookie的内容主要包括:名字,值,过期时间,路径和域。路径与域一起构成cookie的作用范围。

2.1 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就能够一直存在了。

2.2 cookie的有效路径

比如把cookie的有效路径设置为”/xdp”,那么浏览器访问”xdp”目录下的web资源时,都会带上cookie,再比如把cookie的有效路径设置为”/xdp/gacl”,那么浏览器只有在访问”xdp”目录下的”gacl”这个目录里面的web资源时才会带上cookie一起访问,而当访问”xdp”目录下的web资源时,浏览器是不带cookie的

2.3 cookie例子

response接口也中定义了一个addCookie方法,它用于在其响应头中增加一个相应的Set-Cookie头字段。 同样,request接口中也定义了一个getCookies方法,它用于获取客户端提交的Cookie。

2.3.1使用cookie记录用户上一次访问的时间
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        //设置服务器端以UTF-8编码进行输出
        response.setCharacterEncoding("UTF-8");
        //设置浏览器以UTF-8编码进行接收,解决中文乱码问题
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        //获取浏览器访问访问服务器时传递过来的cookie数组
        Cookie[] cookies = request.getCookies();
        //如果用户是第一次访问,那么得到的cookies将是null
        if (cookies != null) {
            out.write("您上次访问的时间是:");
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                if (cookie.getName().equals("lastAccessTime")) {
                    Long lastAccessTime = Long.parseLong(cookie.getValue());
                    Date date = new Date(lastAccessTime);
                    out.write(date.toLocaleString());
                }
            }
        } else {
            out.write("这是您第一次访问本站!");
        }

        //用户访问过之后重新设置用户的访问时间,存储到cookie中,然后发送到客户端浏览器
        Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis() + "");//创建一个cookie,cookie的名字是lastAccessTime
        //将cookie对象添加到response对象中,这样服务器在输出response对象中的内容时就会把cookie也输出到客户端浏览器
        //cookie.setMaxAge(2);设置cookie的有效时间为2S
        response.addCookie(cookie);
    }

第一次访问时这个Servlet时,效果如下所示:
这里写图片描述
第一次请求中无cookie,在响应中要求浏览器设置一个名为”lastAccessTime”的cookie.
Jsessionid只是tomcat的对sessionid的叫法,其实就是sessionid;在其它的容器也许就不叫jsessionid了。(session一般使用cookie技术实现)

__guid:全球唯一标识符 (GUID) 是一个字母数字标识符。这个参数用户不必关心,与浏览器相关,这两个参数是360浏览器上的,在Edge上没有,在Chrome上表现形式也不同,用户不必关心。
在Edge上:
这里写图片描述
在Chrome上:
这里写图片描述
点击浏览器的刷新按钮,进行第二次访问,此时就服务器就可以通过cookie获取浏览器上一次访问的时间了,效果如下:
这里写图片描述

2.3.2 设置有效时间

在上面的例子中,在程序代码中并没有使用setMaxAge方法设置cookie的有效期,所以当关闭浏览器之后,cookie就失效了,要想在关闭了浏览器之后,cookie依然有效,那么在创建cookie时,就要为cookie设置一个有效期。如下所示:

cookie.setMaxAge(2);//设置过期时间为2S

这里写图片描述
cookie一般存储在本地的sqlite数据库中。

2.3.3 Cookie注意细节
  • 一个Cookie只能标识一种信息,它至少含有一个标识该信息的名称(NAME)和设置值(VALUE)。
  • 一个WEB站点可以给一个WEB浏览器发送多个Cookie,一个WEB浏览器也可以存储多个WEB站点提供的Cookie。
  • 浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB
  • 如果创建了一个cookie,并将他发送到浏览器,默认情况下它是一个会话级别的cookie(即存储在浏览器的内存中),用户退出浏览器之后即被删除。若希望浏览器将该cookie存储在磁盘上,则需要使用maxAge,并给出一个以秒为单位的时间。将最大时效设为0则是命令浏览器删除该cookie。
  • 将cookie的有效期设置为0,命令浏览器删除该cookie
  • cookie中存取中文
    要想在cookie中存储中文,那么必须使用URLEncoder类里面的encode(String s, String enc)方法进行中文转码,例如:
Cookie cookie = new Cookie("userName", URLEncoder.encode("孤傲苍狼", "UTF-8"));
response.addCookie(cookie);

在获取cookie中的中文数据时,再使用URLDecoder类里面的decode(String s, String enc)进行解码,例如:

URLDecoder.decode(cookies[i].getValue(), "UTF-8")

3. Session

在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,为用户服务。

3.1 Session和Cookie的主要区别

Cookie是把用户的数据写给用户的浏览器。
Session技术把用户的数据写到用户独占的session中。
Session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。

3.2 例子

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        //使用request对象的getSession()获取session,如果session不存在则创建一个
        HttpSession session = request.getSession();
        //将数据存储到session中
        session.setAttribute("data", "jack cao");
        //获取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);
        }

    }

第一次访问时,服务器会创建一个新的sesion,并且把session的Id以cookie的形式发送给客户端浏览器,如下图所示:
这里写图片描述
点击刷新按钮,再次请求服务器,此时就可以看到浏览器再请求服务器时,会把存储到cookie中的session的Id一起传递到服务器端了,如下图所示:
这里写图片描述
我猜想request.getSession()方法内部新创建了Session之后一定是做了如下的处理

//获取session的Id
String sessionId = session.getId();
//将session的Id存储到名字为JSESSIONID的cookie中
Cookie cookie = new Cookie("JSESSIONID", sessionId);
//设置cookie的有效路径
cookie.setPath(request.getContextPath());
response.addCookie(cookie);

session对象默认30分钟没有使用,则服务器会自动销毁session,在web.xml文件中可以手工配置session的失效时间

3.2 浏览器禁用Cookie后的session处理

URL重写
response.encodeRedirectURL(java.lang.String url) 用于对sendRedirect方法后的url地址进行重写。
response.encodeURL(java.lang.String url)用于对表单action和超链接的url地址进行重写
范例:禁用Cookie后servlet共享Session中的数据
模拟一个购物网站,记住用户购买记录的行为,如果浏览器开启cookie则使用session很容易实现,但是客户端禁用cookie时,就使用URL重写,在URL中增加sessionId,这样也达到了相同的效果。
购物网站首页:

package demo1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class Servlet1 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        doGet(request, response);
    }
    //首页:列出所有书
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            //创建Session
            request.getSession();
            out.write("本网站有如下书:<br/>");
            Set<Map.Entry<String,Book>> set = DB.getAll().entrySet();
            for(Map.Entry<String,Book> me : set){
                Book book = me.getValue();
                String url =request.getContextPath()+ "/BuyServlet?id=" + book.getId();
                //response. encodeURL(java.lang.String url)用于对表单action和超链接的url地址进行重写
                url = response.encodeURL(url);//将超链接的url地址进行重写
                out.println(book.getName()  + "   <a href='"+url+"'>购买</a><br/>");
            }
        }

}


/**
 * 模拟数据库
 */
class DB{
    private static Map<String,Book> map = new LinkedHashMap<String,Book>();
    static{
        map.put("1", new Book("1","javaweb开发"));
        map.put("2", new Book("2","spring开发"));
        map.put("3", new Book("3","hibernate开发"));
        map.put("4", new Book("4","struts开发"));
        map.put("5", new Book("5","ajax开发"));
    }

    public static Map<String,Book> getAll(){
        return map;
    }
}

class Book{

    private String id;
    private String name;

    public Book() {
        super();
    }
    public Book(String id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

页面效果
这里写图片描述
BuyServlet

package demo1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class BuyServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String id = request.getParameter("id");
        Book book = DB.getAll().get(id);  //得到用户想买的书
        HttpSession session = request.getSession();
        List<Book> list = (List) session.getAttribute("list");  //得到用户用于保存所有书的容器
        if(list==null){
            list = new ArrayList<Book>();
            session.setAttribute("list", list);
        }
        list.add(book);
        //response. encodeRedirectURL(java.lang.String url)用于对sendRedirect方法后的url地址进行重写
        String url = response.encodeRedirectURL(request.getContextPath()+"/ListCartServlet");
        System.out.println(url);
        response.sendRedirect(url);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}

ListCartServlet

package demo1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

public class ListCartServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        HttpSession session = request.getSession();
        List<Book> list = (List) session.getAttribute("list");
        if(list==null || list.size()==0){
            out.write("对不起,您还没有购买任何商品!!");
            return;
        }

        //显示用户买过的商品
        out.write("您买过如下商品:<br>");
        for(Book book : list){
            out.write(book.getName() + "<br/>");
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

在禁用了cookie的下的运行效果如下:

这里写图片描述
每一个超链接后面都带上了session的Id
这里写图片描述

所以,当浏览器禁用了cookie后,就可以用URL重写这种解决方案解决Session数据共享问题。而且response. encodeRedirectURL(java.lang.String url) 和response. encodeURL(java.lang.String url)是两个非常智能的方法,当检测到浏览器没有禁用cookie时,那么就不进行URL重写了。

4. 使用Session防止表单重复提交

在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用户可能会以为是自己没有提交表单,就会再点击提交按钮重复提交表单,我们在开发中必须防止表单重复提交。
如果没有进行form表单重复提交处理,那么在网络延迟的情况下下面的操作将会导致form表单重复提交多次

有如下的form.jsp页面

4.1 表单重复提交的常见应用场景

场景一:在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交
场景二:表单提交后用户点击【刷新】按钮导致表单重复提交
点击浏览器的刷新按钮,就是把浏览器上次做的事情再做一次,因为这样也会导致表单重复提交。
场景三:用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交

4.2 利用JavaScript防止表单重复提交

既然存在上述所说的表单重复提交问题,那么我们就要想办法解决,比较常用的方法是采用JavaScript来防止表单重复提交,具体做法如下:

修改form.jsp页面,添加如下的JavaScript代码来防止表单重复提交

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
    <title>Form表单</title>
    <head>
        <title>Form表单</title>
        <script type="text/javascript">
            var isCommitted = false;//表单是否已经提交标识,默认为false
            function dosubmit(){
                if(isCommitted==false){
                    isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true
                    return true;//返回true让表单正常提交
                }else{
                    return false;//返回false那么表单将不提交
                }
            }
        </script>
</head>

<body>
<form action="${pageContext.request.contextPath}/demo1" method="post">
    用户名:<input type="text" name="username">
    <input type="submit" value="提交" id="submit">
</form>
</body>
</html>

可以看到,针对”在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交”这个应用场景,使用JavaScript是可以解决这个问题的,解决的做法就是”用JavaScript控制Form表单只能提交一次”。

除了用这种方式之外,经常见的另一种方式就是表单提交之后,将提交按钮设置为不可用,让用户没有机会点击第二次提交按钮,代码如下:

function dosubmit(){
    //获取表单提交按钮
    var btnSubmit = document.getElementById("submit");
    //将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
    btnSubmit.disabled= "disabled";
    //返回true让表单可以正常提交
    return true;
}

使用JavaScript防止表单重复提交的做法只对上述提交到导致表单重复提交的三种场景中的【场景一】有效,而对于【场景二】和【场景三】是没有用,依然无法解决表单重复提交问题。

4.3 利用Session防止表单重复提交

对于【场景二】和【场景三】导致表单重复提交的问题,既然客户端无法解决,那么就在服务器端解决,在服务器端解决就需要用到session了。

具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号
当浏览器第一次提交表单时,服务端发现token一致,进行正常处理,处理完之后把服务端的tonken删除;
同一页面,当浏览器第二次提交表单时,由于服务器的token已经被删除,所以必然不一致,所以就识别为重复提交。

在下列情况下,服务器程序将拒绝处理用户提交的表单请求:
存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
当前用户的Session中不存在Token(令牌)。
用户提交的表单数据中没有Token(令牌)。
看具体的范例:
创建Servlet,用于生成Token(令牌)和跳转到form.jsp页面

package demo1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

public class Servlet1 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
        if (b == true) {
            System.out.println("请不要重复提交");
            return;
        }
        request.getSession().removeAttribute("token");//移除session中的token
        System.out.println("处理用户提交请求!!");
    }

    /**
     * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
     * @return true 用户重复提交了表单
     * false 用户没有重复提交表单
     */
    private boolean isRepeatSubmit(HttpServletRequest request) {
        String client_token = request.getParameter("token");
        //1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
        if (client_token == null) {
            return true;
        }
        //取出存储在Session中的token
        String server_token = (String) request.getSession().getAttribute("token");
        //2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
        if (server_token == null) {
            return true;
        }
        //3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
        if (!client_token.equals(server_token)) {
            return true;
        }

        return false;
    }


    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String token = UUID.randomUUID().toString();//创建令牌
        System.out.println("在FormServlet中生成的token:" + token);
        request.getSession().setAttribute("token", token);  //在服务器使用session保存token(令牌)
        request.getRequestDispatcher("/views/form.jsp").forward(request, response);//跳转到form.jsp页面
    }

}

在form.jsp中使用隐藏域来存储Token(令牌)

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>form表单</title>
</head>

<body>
<form action="${pageContext.request.contextPath}/demo1" method="post">
    <%--使用隐藏域存储生成的token--%>
    <%--
        <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
    --%>
    <%--使用EL表达式取出存储在session中的token--%>
    <input type="hidden" name="token" value="${token}"/>
    用户名:<input type="text" name="username">
    <input type="submit" value="提交">
</form>
</body>
</html>

这里写图片描述

参考文献

更多推荐