Session 对象学习笔记

session对象

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

  2. session和cookie的主要区别在于:cookie是把用户的数据写给用户的浏览器(保存在客户机);session技术把用户的数据写到用户独占的session中(保存在服务器)。

  3. session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。

session实现原理

  1. 浏览器A第一次访问Servlet1,服务器会创建一个session,每个session都有一个id号,创建好了后,服务器将id号以cookie的形式回送给客户机(这些是服务器自动完成的)。

  2. 当浏览器未关闭前再次发请求访问Servlet2时,就会带着这个id号去访问服务器,这时候服务器检索下内存中有没有与之对应的session,有就用这个session为其服务。

  3. 如果想要关掉浏览器再打开还可以使用同一个session,则需要给服务器回送的cookie设置有效时间(服务器自动回送的时候是没有有效期的)。具体做法是通过session对象的getId方法获得该session的id,然后创建一个cookie,该cookie的名字为"JSESSIONID",值就是刚刚获得的id,再将该cookie设置下有效期,(也可以设置下Path),并添加到cookie中即可。但是有效期不得超过30分钟,因为浏览器关掉后,session只保存30分钟。


案例1

要求:通过三个servlet来实现简单的购物功能。

  1. 创建一个保存书籍信息的类,以及数据类型

    public class Book {
        private String id;
        private String name;
        private String author;
        private String description;
    
        public Book() {
        }
    
        public Book(String id, String name, String author, String description) {
            this.id = id;
            this.name = name;
            this.author = author;
            this.description = description;
        }
    
        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;
        }
    
        public String getAuthor() {
            return author;
        }
    
        public void setAuthor(String author) {
            this.author = author;
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    }
    
    public class Dao {
        private static Map<String, Book> map = new LinkedHashMap();
    
        // 静态代码块中的内容只执行一次,该类在加载时,往map集合中put一系列书,map也需要设置为静态的
        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开发", "老张", "一本好书"));
            map.put("6", new Book("6", "Java基础", "老孙", "一本好书"));
        }
    
        public static Map getAll() {
            return map;
        }
    
    }
    
  2. IndexServlet显示首页,并列出所有书

    @WebServlet("/indexServlet")
    public class IndexServlet extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            // 设置响应的消息体的数据格式以及编码
            response.setContentType("text/html;charset=UTF-8");
            // 定义 response.getWriter() 对象
            PrintWriter out = response.getWriter();
    
            out.write("本网站有如下书:<br/>");
    
            // 获取每一本书对应的键值
            Set<Map.Entry<String,Book>> keySet = Dao.getAll().keySet();
    
            for (Map.Entry<String, Book> stringBookEntry : keySet) {
    
                Book book = stringBookEntry.getValue();
                // 定义标签体内容,指定购买的书:用户点击购买,指定给BuyServlet类去处理
                out.write(
                        book.getName() +
                                "<a href='/SessionTest_war_exploded/buyServlet?id="
                                + book.getId() + "'>购买</a><br/>"
                );
                System.out.println(book.getName() + ":" + book.getId());
            }
        }
    
        @Override
        public void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            this.doGet(request, response);
        }
    }
    
  3. 当用户点击购买时,将书的id号带上,并跳转到BuyServlet去处理

    @WebServlet("/buyServlet")
    public class BuyServlet extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            // 获得url中带过来的参数id
            String id = request.getParameter("id");
            // 在Dao中获得该id号的book
            Book book = (Book)Dao.getAll().get(id);
            // 获得当前session对象
            HttpSession session = request.getSession();
            // 设置新的cookie,注意cookie名必须为JSESSIONID,值为该session的id
            Cookie cookie = new Cookie("JSESSIONID", session.getId());
            // 设置cookie的有效期
            cookie.setMaxAge(30 * 60);
            // 设置cookie的路径
            cookie.setPath("/SessionTest_war_exploded");
            // 将cookie添加到cookies中带给浏览器,下次浏览器访问,就会将此cookie带过来了
            response.addCookie(cookie);
            // 先把书加到容器里,再把容器加到session中。一般先检查用户的session中有没有保存书的容器,没有就创建,有就加
            List list = (List)session.getAttribute("list");
            if(list == null) {
                list = new ArrayList();
                session.setAttribute("list", list);
            }
            list.add(book);
            // 跳转到显示用户买过哪些商品
            response.sendRedirect("/SessionTest_war_exploded/listCartServlet");
        }
    
        @Override
        public void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            this.doGet(request, response);
        }
    }
    
  4. 从上面的程序可以看出,当用户点击购买后,会将书的id号带过来,我们拿到id号后就可以找到相应的书,同时我们将当前session的id保存到cookie中,再带给浏览器,这样下次浏览器访问的时候就会将当前session的id带过来了。拿到相应的书后,放到list中,再把list放到session中,这样下次跳转的时候,浏览器带来的cookie中有当前session的id,我们可以通过getSession()获得当前的session,再把session中保存的list拿出来,就知道用户买了哪些书了。这就是购物车的原理。

    @WebServlet("/listCartServlet")
    public class ListCartServlet extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            // 设置响应的消息体的数据格式以及编码
            response.setContentType("text/html;charset=UTF-8");
            // 定义 response.getWriter() 对象
            PrintWriter out = response.getWriter();
            // 获得当前的session
            HttpSession session = request.getSession();
            // 从session中拿出list
            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/>");
            }
        }
    
        @Override
        public void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            this.doGet(request, response);
        }
    }
    
  5. 启动服务器,浏览器访问:http://localhost:8080/SessionTest_war_exploded/indexServlet

    20200602155253
  6. 没有购买过书籍,访问:http://localhost:8080/SessionTest_war_exploded/buyServlet

    20200602155457
  7. 点击购买一本书,页面跳转到:http://localhost:8080/SessionTest_war_exploded/indexServlet/buyServlet,然后跳转到:http://localhost:8080/SessionTest_war_exploded/listCartServlet

    20200602155723
  8. 由于session是保存在服务器端的,cookie是保存在浏览器端的。这里的session和cookie只是保存在内存中,并没有因为设置cookie的保存时间问题二长期存储到硬盘中,如果关闭某一端,购买的物品都会失效。(不知是否正确,待证明)

细节说明

  1. 当客户端关闭后,服务器不关闭,两次获取session,默认情况下不是同一个session。如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。

    Cookie cookie = new Cookie("JSESSIONID",session.getId());
    cookie.setMaxAge(60 * 60);
    response.addCookie(c);
    

  2. 客户端不关闭,服务器关闭后,两次获取的session,不是同一个,但是要确保数据不丢失:

    1. session的钝化:

      在服务器正常关闭之前,将session对象系列化到硬盘上。

    2. session的活化:

      在服务器启动后,将session文件转化为内存中的session对象即可。

    3. IDEA 不能实现session的活化:

      • 从tomcat的文件目录中可以知道,word 目录是用来存储程序运行中动态生成的数据(JSP转换的Java文件、Session被序列化之后的文件 ...... ) 。
      • 将项目打包成wer包,放到tomcat中,在终端中运行tomcat服务器,之后正常关闭tomcat服务器,会在word/Catalina/locathout/项目名称/目录中生成SESSIONS.ser文件, 该文件放的就是session对象,重新启动服务器,再次访问该项目,该SESSIONS.ser文件就会被自动读取进内容,且会将SESSIONS.ser文件从硬盘中删除 。
      • IDEA 虽然可以进行钝化,但是不能进行活化。在IDEA中启动tomcat,找到对应该项目的目录,可以发现会生成一个word目录,这里和终端中启动tomcat,也是一样的(即生成一个word目录)。同理后面在IDEA中关闭tomcat,对应的项目目录下的 word/Catalina/locathout/项目名称/ 目录也会生成SESSIONS.ser文件。不过再次从IDEA中启动tomcat服务器,那么这里就不一样了,之前word目录会被删除,然后创建一个新的word目录。那之前生成的SESSIONS.ser文件也会被删除,就不能读取到那个 SESSIONS.ser 文件了,也就不能实现session的活化了。
  3. Session的失效时间

    • 服务器关闭

    • session对象调用invalidate()

    • session默认失效时间 30分钟,也可以修改对应的web.xml文件中的如下内容,来自定义失效时间:

      <session-config>
          <session-timeout>30</session-timeout>
      </session-config>
      

  4. Session的特点

    1. session用于存储一次会话的多次请求的数据,存在服务器端。

    2. session可以存储任意类型,任意大小的数据。

  5. session与Cookie的区别

    1. session存储数据在服务器端,Cookie在客户端
    2. session没有数据大小限制,Cookie有大小限制
    3. session数据安全,Cookie相对于不安全

案例2

案例需求

  1. 访问带有验证码的登录页面login.jsp
  2. 用户输入用户名,密码以及验证码。
    • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
    • 如果验证码输入有误,跳转登录页面,提示:验证码错误
    • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您

案例分析

  1. 用户访问login.jsp登录页面,输入用户名、密码和验证码,点击登录
  2. 用户点击登录后,将请求提交给 LoginServlet.java 处理
    1. 设置request编码
    2. 获取参数Map集合
    3. 获取验证码
    4. 将用户信息封装到User对象
    5. 判断Java程序生成的验证码和用户输入的验证码是否一致
      1. 从session中获取Java程序生成的验证码。
      2. 如果生成的验证码和用户输入的验证码一致,再判断用户名和密码是否正确
        1. 如果用户名和密码正确,用户登录成功,页面跳转到success.jsp,存储用户登录的数据(采用重定向)。
        2. 如果用户名和密码不正确,login.jsp页面给用户提示信息:用户名或密码错误。
      3. 如果生成的验证码和用户输入的验证码不一致,login.jsp页面给用户提示信息,验证码输入错误,并刷新验证码
  3. 查询用户名和密码是否正确的时候,将从 LoginServlet 中获取到的用户名和密码与 UserDao.java 中存储到的用户名和密码对比,判断是否正确。

案例的实现

  1. 验证码的实现:CheckCodeServlet.java

    @WebServlet("/checkCodeServlet")
    public class CheckCodeServlet extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            // 定义验证码方框长和宽
            int width = 100, height = 50;
            // 创建一个对象,在内存中画图(验证码图片对象)
            BufferedImage bufferedImage =
                    new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
            // 美化图片
            // 创建画笔对象
            Graphics graphics = bufferedImage.getGraphics();
            // 设置画笔的颜色
            graphics.setColor(Color.PINK);
            // 用画笔,将制定的长方形区域,画满(画验证码图片背景 -> 粉红色)
            graphics.fillRect(0, 0, width, height);
            // 画验证码图片边框
            // 设置画笔的颜色
            graphics.setColor(Color.BLACK);
            graphics.drawRect(0, 0, width - 1, height - 1);
            // 定义一个包含所有字母和数字的字符串(验证码)
            String strings = "QqWwEeRrTtYyUuIiOoPpAaSsDdFfGgHhJjKkLlZzXxCcVvBbNnMm1234567890";
            // 创建随机数对象,用来获取字符串中的一个字符
            Random random = new Random();
            // 写入四个字符在验证码方框中
            int codeNumber = 4;
            // 创建字符缓冲区对象,存储验证码
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 1; i <= codeNumber; i++) {
                // strings 索引
                int index = random.nextInt(strings.length());
                // 通过索引获取字符
                char indexString = strings.charAt(index);
                // 将一个验证码字符存储到字符缓冲区
                stringBuilder.append(indexString);
                // 写入一个验证符
                graphics.drawString(indexString + "", width / 5 * i, height / 2);
            }
            // 验证码
            String checkCodeSession = stringBuilder.toString();
            // 将验证码存入Session
            request.getSession().setAttribute("checkCodeSession", checkCodeSession);
            // 在验证码方框中画干扰线
            graphics.setColor(Color.GREEN);
            int lineNumber = 10;
            for (int i = 0; i < lineNumber; i++) {
                // 生成随机坐标点
                int x1 = random.nextInt(width);
                int x2 = random.nextInt(width);
                int y1 = random.nextInt(height);
                int y2 = random.nextInt(height);
                // 画线
                graphics.drawLine(x1, y1, x2, y2);
            }
    
            // 将图片输出到页面展示
            ImageIO.write(bufferedImage, "png", response.getOutputStream());
        }
    
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            this.doGet(request, response);
        }
    }
    
  2. 创建并编写一个登录页面:Login.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Login</title>
        <script type="text/javascript" src="LoginScript.js"></script>
        <style>
            div {
                color: pink
            }
        </style>
    </head>
    <body>
        <!-- 创建一个表单,指向 LoginServlet.java -->
        <form action="/SessionMaster_war_exploded/loginServlet" method="post">
            <table>
                <tr>
                    <td>用户名</td>
                    <td><input type="text" name="username" placeholder="请输入用户名"></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input type="password" name="password" placeholder="请输入密码"></td>
                </tr>
                <tr>
                    <td>验证码</td>
                    <td><input type="text" name="checkCode" placeholder="请输入验证码"></td>
                </tr>
                <!--
                验证码图片,指向CheckCodeServlet.java,该验证码图片占两个单元格
                且绑定单击事件,交给LoginScript.js中的ClickImage()方法处理
                -->
                <tr>
                    <td colspan="2"><img id="clickCheckCodeImage" onclick="ClickImage()" src="/SessionMaster_war_exploded/checkCodeServlet"></td>
                </tr>
                <tr>
                    <td><input type="submit" value="登录"></td>
                </tr>
            </table>
        </form>
    
        <%-- 获取错误信息 --%>
        <div><%= request.getAttribute("usernameOrPasswordError") == null ? "" : request.getAttribute("usernameOrPasswordError") %></div>
        <div><%= request.getAttribute("checkCodeError") == null ? "" : request.getAttribute("checkCodeError")%></div>
    
    </body>
    </html>
    
  3. 从上面的 Login.jsp 可以看出,验证码是由 CheckCodeServlet.java 生成的,且绑定了单击事件,交给 LoginScript.js 处理。表单提交后,交给 LoginServlet.java 处理。

    1. 编写 LoginScript.js

      function ClickImage() {
          // 验证码,绑定鼠标单击事件,用户点击验证码图片,验证码自动更新
          document.getElementById("clickCheckCodeImage").onclick = function code() {
              this.src = "/SessionMaster_war_exploded/checkCodeServlet?time="+new Date().getTime();
          }
      }
      
    2. 编写 LoginServlet.java

      @WebServlet("/loginServlet")
      public class LoginServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest request, HttpServletResponse response)
                  throws ServletException, IOException {
              // 设置request编码
              request.setCharacterEncoding("utf-8");
              // 获取参数,保存在Map集合中
              Map<String, String[]> parameterMap = request.getParameterMap();
              // 获取 Map 集合中的所有键值
              Set<String> strings = parameterMap.keySet();
              // 获取username、password、checkCode
              String username = null, password = null, checkCode = null;
              // 获取Set集合的迭代器对象,并遍历该迭代器对象
              Iterator<String> ite = strings.iterator();
              while (ite.hasNext()) {
                  // 通过迭代器对象中的每一个值(Map集合中的键),获取value
                  String next = ite.next();
                  if ("username".equals(next)) {
                      username = parameterMap.get(next)[0];
                  } else if ("password".equals(next)) {
                      password = parameterMap.get(next)[0];
                  } else if ("checkCode".equals(next)) {
                      checkCode = parameterMap.get(next)[0];
                  }
              }
              // 通过session对象获取生成的验证码
              HttpSession session = request.getSession();
              String checkCodeSession = (String) session.getAttribute("checkCodeSession");
              // 获取完验证码后,将原有的验证码从session中删除(保证验证码只能被使用一次)
              session.removeAttribute("checkCodeSession");
              // 判断用户输入的验证码是否和生成的验证码一致(不区分大小写)
              if (checkCodeSession != null && checkCodeSession.equalsIgnoreCase(checkCode)) {
                  // 验证码一致,判断用户名和密码是否正确
                  boolean flag = false;
                  String[] userInfoArrayList = UserDao.getNamePassword();
                  for (String userInfo : userInfoArrayList) {
                      if ((username + password).equals(userInfo)) {
                          flag = true;
                      }
                  }
                  if (flag == true) {
                      // 用户名和密码正确
                      // 存储用户名信息到session
                      session.setAttribute("username", username);
                      // 重定向到 Success.jsp
                      response.sendRedirect(request.getContextPath()+"/Success.jsp");
                  } else {
                      // 用户名和密码不正确,存储提示信息到request
                      request.setAttribute("usernameOrPasswordError", "用户名或密码错误,请检查后修改!");
                      // 将该信息转发到登录页面
                      request.getRequestDispatcher("/Login.jsp").forward(request, response);
                  }
              } else {
                  if (checkCodeSession == null) {
                      // 验证码只能使用一次,假如登陆成功后,返回前一个页面,验证码图片是没有更新的。
                      // 要保证验证码只能使用一次。存储提示信息到request
                      request.setAttribute("checkCodeError", "验证码已经失效,请刷新验证码!");
                  } else {
                      // 验证码不一致,存储提示信息到request
                      request.setAttribute("checkCodeError", "验证码输入错误,请重新输入验证码");
                  }
                  // 将该信息转发到登录页面
                  request.getRequestDispatcher("/Login.jsp").forward(request, response);
              }
          }
      
          @Override
          protected void doPost(HttpServletRequest request, HttpServletResponse response)
                  throws ServletException, IOException {
              this.doGet(request, response);
          }
      }
      
    3. LoginServlet.java 可以知道,该类中的判断使用了 UserDao.java 中的信息。如果用户登录成功,页面将挑战到 Success.jsp

  4. 编写 UserDao.java

    1. 编写自定义用户信息数据类型:UserInfo.java

      public class UserInfo {
          private String username;
          private String password;
          private String gender;
      
          public UserInfo() { }
      
          public UserInfo(String username, String password, String gender) {
              this.username = username;
              this.password = password;
              this.gender = gender;
          }
      
          public String getUsername() { return username; }
      
          public void setUsername(String username) { this.username = username; }
      
          public String getPassword() { return password; }
      
          public void setPassword(String password) { this.password = password; }
      
          public String getGender() { return gender; }
      
          public void setGender(String gender) { this.gender = gender; }
      }
      
    2. 编写UserDao.java

      public class UserDao {
          private static Map<Integer, UserInfo> userInfoArrayList = new LinkedHashMap();
      
          static {
              userInfoArrayList.put(0, new UserInfo("LeeHua", "2020520", "male"));
              userInfoArrayList.put(1, new UserInfo("Rainbow", "20181314", "female"));
          }
      
          public static String[] getNamePassword() {
              String[] namesAndPasswords = new String[2];
              // 获取 Map 集合中的所有键值
              Set<Integer> strings = userInfoArrayList.keySet();
              // 获取Set集合的迭代器对象,并遍历该迭代器对象
              Iterator<Integer> ite = strings.iterator();
              while (ite.hasNext()) {
                  // 通过迭代器对象中的每一个值(Map集合中的键),获取value
                  Integer next = ite.next();
                  String username = userInfoArrayList.get(next).getUsername();
                  String password = userInfoArrayList.get(next).getPassword();
                  namesAndPasswords[next] = username + password;
              }
              return namesAndPasswords;
          }
      
      }
      
  5. 该案例的代码部分已经基本完成,下面进行测试。

案例测试

  1. 启动浏览器,访问:http://localhost:8080/SessionMaster_war_exploded/Login.jsp

    20200604022302
  2. 用户名和密码信息,在 UserDao.java 类中,如下:

    UserInfo("LeeHua", "2020520", "male")
    UserInfo("Rainbow", "20181314", "female")
    
  3. 输入正确的用户名和验证码:如LeeHua、2020520

    页面跳转到:http://localhost:8080/SessionMaster_war_exploded/Success.jsp

    20200604022625
  4. 返回前一个页面,会发现,验证码是没有发生改变的,假如没有点击验证码图片来刷新验证码,而继续选择登录,无论用户名或者密码是否正确,都会出现以下情况:

    1. 页面跳转到:http://localhost:8080/SessionMaster_war_exploded/loginServlet

    2. 页面显示内容如下:

      20200604022935
  5. 无论用户名是否正确,都会先判断验证码的正确性,验证码正确的情况下才会继续判断用户名和密码是否正确。

  6. 验证码错误的情况:

    1. 输入:

      20200604023311
    2. 页面跳转到:http://localhost:8080/SessionMaster_war_exploded/loginServlet

    3. 页面显示情况:

      20200604023431
  7. 密码或用户名错误的情况:

    1. 输入:

      20200604023646
    2. 页面跳转到:http://localhost:8080/SessionMaster_war_exploded/loginServlet

    3. 页面显示情况:

      20200604023707

参考文献

  1. 会话管理之session技术
posted @ 2021-03-04 10:32  LeeHua  阅读(449)  评论(0编辑  收藏  举报