• JavaWeb 学习笔记 6:会话跟踪


    JavaWeb 学习笔记 6:会话跟踪

    HTTP 协议本身是无状态的,所以不能跟踪会话状态。所以会有额外的技术用于跟踪会话:

    • Cookie,客户端技术
    • Session,服务端技术

    1.Cookie

    1.1.写入 Cookie

    可以在服务端通过HttpServletResponse.addCookie向浏览器写入 Cookie:

    @WebServlet("/a")
    public class ControllerA extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // 向浏览器添加 cookie
            Cookie cookie = new Cookie("username", "icexmoon");
            Cookie cookie1 = new Cookie("msg", "hello");
            resp.addCookie(cookie);
            resp.addCookie(cookie1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请求 http://localhost:8080/session-demo/a 能看到响应报文头:

    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    Set-Cookie: username=icexmoon
    Set-Cookie: msg=hello
    Content-Length: 0
    Date: Mon, 11 Sep 2023 09:39:41 GMT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用开发者工具可以看到浏览器端 Cookie 已添加:

    image-20230911174548444

    应当注意到,服务端添加的 Cookie 默认的存活时间(Expire / Max age)默认是会话,即会话结束(关闭浏览器)后 Cookie 就会被销毁。此时 Cookie 仅保存在内存中,并不会被持久化保存(保存到硬盘)。

    使用Cookie.setMaxAge可以设置 Cookie 的生存时间(单位:秒):

    // 向浏览器添加 cookie
    Cookie cookie = new Cookie("username", "icexmoon");
    Cookie cookie1 = new Cookie("msg", "hello");
    // 设置有效时间为 1 天
    cookie.setMaxAge(1 * 24 * 60 * 60);
    resp.addCookie(cookie);
    resp.addCookie(cookie1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    响应报文:

    Set-Cookie: username=icexmoon; Expires=Tue, 12-Sep-2023 09:54:25 GMT
    Set-Cookie: msg=hello
    
    • 1
    • 2

    响应报文中的 Cookie 有效期是直接以截至时间的方式返回的:

    Expires=Tue, 12-Sep-2023 09:54:25 GMT
    
    • 1

    这是格林尼治时间(GMT),换算成中国时间(东八区)要+8小时。

    用开发者工具查看就能看到有效期已经改变:

    image-20230911175940484

    有效期可以设置为以下几种:

    • 正数,在X秒后过期
    • 0,立即过期(删除)
    • 负数,会话有效期,在会话结束(浏览器退出)后过期

    1.2.读取 Cookie

    使用HttpServletRequest.getCookies可以读取浏览器传递的 Cookie:

    @WebServlet("/b")
    public class ControllerB extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            Cookie[] cookies = req.getCookies();
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("username")) {
                    String username = cookie.getValue();
                    System.out.println("username: " + username);
                    break;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    请求 http://localhost:8080/session-demo/b 就能看到服务端输出的 Cookie 内容。

    查看请求报文:

    GET /session-demo/b HTTP/1.1
    Cookie: JSESSIONID=20C2014C72F0D7ED4D34B821B9A0BC89; username=icexmoon; msg=hello; sentinel_dashboard_cookie=69C1AF3B99482E641CDD23041937F691; JSESSIONID=6EEEF7C596140410E7A21F9DAECF4525
    ...
    
    • 1
    • 2
    • 3

    当前域名下的所有 Cookie 都以Cookie: xxx=xxx; xxx=xxx 这样的请求头传递。

    1.3.中文 Cookie

    HTTP 协议规定,报文头内容只能是 ASCII 字符集的字符,所以如果尝试写入中文的 Cookie 信息(UTF-8 字符集)就会报错:

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Cookie cookie = new Cookie("username", "魔芋红茶");
        response.addCookie(cookie);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    错误信息:

    java.lang.IllegalArgumentException: Control character in cookie value or attribute.
    
    • 1

    所以要将 UTF-8 字符串转换为全部由 ASCII 字符组成的字符串才能作为 Cookie 内容传递。有多种编码可以实现这一点,最常用的有 URL 编码和 Base64 编码。

    这里用 URL 编码举例说明:

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = "魔芋红茶";
        username = URLEncoder.encode(username, StandardCharsets.UTF_8.name());
        Cookie cookie = new Cookie("username", username);
        response.addCookie(cookie);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    响应报文中的信息:

    Set-Cookie: username=%E9%AD%94%E8%8A%8B%E7%BA%A2%E8%8C%B6
    
    • 1

    自然的,在服务端接收到的 Cookie 也是 URL 编码过的,所以需要解码:

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = ServletUtil.getCookie(request, "username");
        if (username!=null){
            username = URLDecoder.decode(username, StandardCharsets.UTF_8.name());
        }
        System.out.println(username);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.Session

    Session 同样可以用于跟踪会话,并保存会话的状态信息,与 Cookie 不同的是,Session 是服务端技术,保存在服务端。

    2.1.写入 Session

    @WebServlet("/e")
    public class ControllerE extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            HttpSession session = request.getSession();
            session.setAttribute("msg", "Hello World!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.2.读取 Session

    @WebServlet("/f")
    public class ControllerF extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            HttpSession session = request.getSession();
            String msg = (String) session.getAttribute("msg");
            System.out.println("Msg in session: " + msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.3.实现原理

    Session 是基于 Cookie 实现的,浏览器端持有的是作为 Cookie 存储的 SessionID,服务端为每个 SessionID 保存对应的 Session 对象,并且可以用浏览器端用 Cookie 方式传递的 SessionID 获取到对应的 Session 对象。

    整个过程可以表示为:

    session原理.drawio

    根据 Session 的实现原理,Session 的有效期也包含两部分:

    • 浏览器端 SessionID 的有效期
    • 服务器端的 Session 对象的有效期

    两者任意一个失效 Session 就不可用了。

    浏览器端的 SessionID 的有效期是会话,即关闭浏览器后就失效:

    image-20230912123211545

    服务器端的 Session 对象由 Web 服务器软件的设置决定,对于 Tomcat,默认的设置为 30 分钟后被清理。需要说明的是,每次有当前会话的请求产生,对应的 Session 对象的过期时间就会刷新,即 +30 分钟。也就是说只要一直有请求,Session 就不会过期,但是如果有超过 30 分钟没有请求,那 Session 对象就会过期被删除。

    之所以为 Session 对象设置有效期,是因为 Session 需要占用服务端内存资源。因此尽量不要为 Session 设置过长的有效期。

    Tomcat 的默认设置在 /conf/web.xml 中:

    <session-config>
        <session-timeout>30session-timeout>
    session-config>
    
    • 1
    • 2
    • 3

    可以通过修改 Web 应用的 web.xml 覆盖 Tomcat 的默认设置:

    <web-app>
      <display-name>Archetype Created Web Applicationdisplay-name>
      <session-config>
        <session-timeout>1session-timeout>
      session-config>
    web-app>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样就可以将 Session 对象的过期时间修改为 1 分钟,1 分钟后再请求就会发现对应的 Session 对象已经获取不到了。

    也可以用 HttpSession.invalidate方法主动让某个 Session 对象过期。

    2.4.Session 的持久化

    Session 对象是保存在内存中的,这意味着服务器重启后之前的 Session 对象将不存在。对此,Tomcat 可以在正常退出时将内存中的 Session 序列化后保存在硬盘上,再次启动后从硬盘加载 Session 对象到内存。

    非常正常退出,比如关闭线程或者服务器电源关闭等无法持久化保存 Session。

    下面用一个简单测试进行验证。

    使用命令行mvn tomcat7:run启动 Web 项目。

    请求 xxx/e后再请求xxx/f,可以看到 session 已经生成,并且可以读取。

    在命令行中按Ctrl+C结束 Tomcat。

    注意把 Session 有效期改回 30 秒,并去除相关主动销毁 Session 的代码。

    此时会在 Tomcat 下的 localhost/session-demo/org 目录下出现一个序列化文件SESSIONS.ser

    image-20230912175034988

    重新启动 Tomcat 后,如果需要使用 Session,Tomcat 会将之前的 Session 对象从序列化文件加载,并删除该序列化文件,因此可以访问之前的 Session。

    2.5.Session 和 Cookie 的区别

    • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
    • 安全性:Cookie不安全,Session安全
    • 数据大小:Cookie最大3KB,Session无大小限制
    • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
    • 服务器性能:Cookie不占服务器资源,Session占用服务器资源

    3.案例:登录注册

    登录和验证的实现都比较简单,这里只说明一下验证码的实现。

    这里使用一个工具类 CheckCodeUtil 实现验证码的生成:

    public class CheckCodeUtil {
        /**
         * 输出随机验证码图片流,并返回验证码值(一般传入输出流,响应response页面端,Web项目用的较多)
         *
         * @param w 宽
         * @param h 高
         * @param os 输出流
         * @param verifySize 验证码位数
         * @return 生成的验证码(字符串)
         * @throws IOException
         */
        public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
            String verifyCode = generateVerifyCode(verifySize);
            outputImage(w, h, os, verifyCode);
            return verifyCode;
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    利用这个工具类生成验证码,并将生成的验证码图片写入响应报文的输出流:

    @WebServlet("/user/check_code")
    public class CheckCodeController extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            OutputStream os = response.getOutputStream();
            String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
            request.getSession().setAttribute("checkCode", checkCode);
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    用于验证的字符串形式的验证码要保存到 Session,以便在收到注册请求时进行验证。

    注册页面用于显示验证码的图片设置src

    <tr>
        <td>验证码td>
        <td class="inputs">
            <input name="checkCode" type="text" id="checkCode">
            <img id="checkCodeImg" src="/login-demo/user/check_code">
            <a href="#" id="changeImg" onclick="refreshCheckCode()">看不清?a>
        td>
    tr>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    现在页面加载时就能显示验证码。为了能点击 看不清 链接时能刷新,需要实现一个替换图片 src 的 js 方法:

    // 刷新验证码
    function refreshCheckCode(){
        $("img#checkCodeImg").attr("src","/login-demo/user/check_code");
    }
    
    • 1
    • 2
    • 3
    • 4

    要注意的是,此时只有在开发者工具选择禁用缓存的情况下才能正常刷新验证码,缓存生效时是不会有效果的,因为验证码图片会被缓存起来,浏览器会直接使用缓存,不会再次请求。

    这就需要让生成验证码图片的 Servlet 返回的响应报文中禁用缓存:

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 浏览器不能缓存验证码 Cache-Control: no-cache
        response.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate");
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在就没有类似的问题了。

    在客户端发起注册请求时检查验证码:

    // 检查验证码是否正确
    String checkCode = (String) request.getSession().getAttribute("checkCode");
    String inputCheckCode = request.getParameter("checkCode");
    if (checkCode == null || inputCheckCode == null){
        throw new RuntimeException("请先输入验证码");
    }
    if (!checkCode.equalsIgnoreCase(inputCheckCode)){
        System.out.println(checkCode);
        System.out.println(inputCheckCode);
        throw new RuntimeException("验证码不正确");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    The End,谢谢阅读。

    本文的完整示例可以从这里获取。

    4.参考资料

  • 相关阅读:
    ocr的场景应用--发票识别
    嵌入式系统中的GPIO控制与应用
    86.(cesium之家)cesium叠加面接收阴影效果(gltf模型)
    C++对象模型(13)-- 构造函数语义学:析构函数
    业务数据分析最佳案例!旅游业数据分析!⛵
    【力扣10天SQL入门】Day2
    x264 编码器 AArch64 汇编函数模块关系分析
    lua-总结2
    LeetCode每日一练 —— OR36 链表的回文结构
    flink java代码在客户端运行
  • 原文地址:https://blog.csdn.net/hy6533/article/details/133160885