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 @   LeeHua  阅读(515)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示

目录导航