likes
comments
collection
share

手写web server: 6-HttpSession

作者站长头像
站长
· 阅读数 11

【声明】文章为本人学习时记录的笔记。 原课程地址:www.liaoxuefeng.com/wiki/154595…

一、前言

HttpSession是Java Web App的一种机制,用于在客户端和服务器之间维护会话状态信息。

原理:

当客户端第一次请求Web应用程序时,服务器会为该客户端创建一个唯一的Session ID,该ID本质上是一个随机字符串,然后,将该ID存储在客户端的一个名为JSESSIONID的Cookie中。与此同时,服务器会在内存中创建一个HttpSession对象,与Session ID关联,用于存储与该客户端相关的状态信息。

当客户端发送后续请求时,服务器根据客户端发送的名为JSESSIONID的Cookie中获得Session ID,然后查找对应的HttpSession对象,并从中读取或继续写入状态信息。

用途

Session主要用于维护一个客户端的会话状态。通常,用户成功登录后,可以通过如下代码创建一个新的HttpSession,并将用户ID、用户名等信息放入HttpSession

定义

HttpSession是HttpServletRequest中定义的一个接口,Java的Web应用调用HttpServletRequestgetSession()方法时,需要返回一个HttpSession的实现类。

HttpSession的生命周期

1、第一次调用req.getSession()时,服务器会为该客户端创建一个新的HttpSession对象;

2、后续调用req.getSession()时,服务器会返回与之关联的HttpSession对象;

3、调用req.getSession().invalidate()时,服务器会销毁该客户端对应的HttpSession对象;

4、当客户端一段时间内没有新的请求,服务器会根据Session超时自动销毁超时的HttpSession对象。

二、目标

在我们的项目中实现HttpSession机制。

1、首次请求首页时,输入username和password,然后保存到HttpSession中,并写入response的header中,用于客户端保留该cooike信息。后续再请求时,自动携带该用户信息,不用再次进行登录。

2、实现对Session生命周期的维护。

三、设计

1、为了实现对Session的管理,设计一个专门管理SessionManager。 实现对增加,移除等操作。

2、为了方便在httpServletRequest中方便通过统一的SessionManager中获取对应的Session。 将SessionManager统一纳入到ServletContext中进行管理。

四、实现

1、SessionManager

手写web server: 6-HttpSession

通过ConcurrentHashMap类型的sessions来维护系统中的sessionId和HttpSession。这里使用ConcurrentHashMap的原因是因为,为了让我们的系统有自动超时销毁Session的能力,这里SessionManager实现了Runnable接口,通过开启新的守护线程来对当前sessions中的HttpSession进行超时检查。 但同时,获取session的动作,还可能会有用户请求线程。所以这里使用ConcurrentHashMap来保证线程安全。

2、在HttpServletRequestImpl中实现getSession逻辑。 这里在HttpServletRequest接口中定义了重载的getSession方法。

一个是:public HttpSession getSession(); 另一个是:public HttpSession getSession(boolean create);

接口上的解释也很清楚:

手写web server: 6-HttpSession

如果create==true时,可能会去创建新的session。并set到response的header中的Set-Cookie中,保存到浏览器端。

    @Override
    public HttpSession getSession(boolean create) {
        // 1、先从请求的cookie种获取seesionId
        String sessionId = null;
        Cookie[] cookies = getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("JSESSIONID".equals(cookie.getName())) {
                    sessionId = cookie.getValue();
                    break;
                }
            }
        }
        if (sessionId == null && !create) {
            return null;
        }
        // 2、创建新的sessionId,具体创建HttpSession的逻辑在 sessionManager.getSession中实现。
        if (sessionId == null) {
            if (this.response.isCommitted()) {
                throw new IllegalStateException("Cannot create session for response is commited.");
            }
            // 从这里可以看出来,sessionId,就是一个随机的字符串
            sessionId = UUID.randomUUID().toString();
            // set cookie:
            String cookieValue = "JSESSIONID=" + sessionId + "; Path=/; SameSite=Strict; HttpOnly";
            this.response.addHeader("Set-Cookie", cookieValue);
        }
        return this.servletContext.sessionManager.getSession(sessionId);
    }

3、SessionManager中的getSession方法。 该方法,先根据sessionId从map中get,如果获取到了,则更新其最后的使用时间(用于守护线程判断是否超时使用);如果get不到,则新建HttpSession,然后put进map中。

    public HttpSession getSession(String sessionId) {
        HttpSessionImpl session = sessions.get(sessionId);
        if (session == null) {
            session = new HttpSessionImpl(this.servletContext, sessionId, inactiveInterval);
            sessions.put(sessionId, session);
        } else {
            session.lastAccessedTime = System.currentTimeMillis();
        }
        return session;
    }

4、session的管理能力实现后,下面就去更新请求流程中的逻辑。 比如,随便请求到我们系统中的某个已有path,首先判断当前会话是否有效。如果过期了,则跳转到登录页。 比如:IndexServlet中的doGet逻辑。

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        String username = (String) session.getAttribute("username");
        String html;
        if (username == null) {
            html = """
                    <h1>Index Page</h1>
                    <form method="post" action="/login">
                        <legend>Please Login</legend>
                        <p>User Name: <input type="text" name="username"></p>
                        <p>Password: <input type="password" name="password"></p>
                        <p><button type="submit">Login</button></p>
                    </form>
                    """;
        } else {
            html = """
                    <h1>Index Page</h1>
                    <p>Welcome, {username}!</p>
                    <p><a href="/logout">Logout</a></p>
                    """.replace("{username}", username);
        }
        resp.setContentType("text/html");
        PrintWriter pw = resp.getWriter();
        pw.write(html);
        pw.close();
    }

通过前言关于HttpSession的生命周期的第2项,我们可以了解到。如果当前我们是在登录后,再次请求到IndexServlet时。通过,req.getSession()时,会返回与之关联的HttpSession对象。从该Session对象中获取username(登录时,会进行设置)标签时,如果没有被超时清除时,会获取到对应的username属性值。 从而实现了Seesion有效期间的免登录目的。

那么为什么通过,req.getSession()时,会返回与之关联的HttpSession对象?

A:因为从前面的req.getSession()的逻辑中可以看到,获取的HttpSession是首先通过解析请求的cookie,并通过检索JSESSIONID来获取唯一的sessionId。相当于是,只要是Session未过期,同一个用户,每次请求都是同一个sessionId,从而保证了,req.getSession()获取到的都是同一个HttpSession实例。

5、LoginServlet的doPost()中增加对session的处理

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        String expectedPassword = users.get(username.toLowerCase());
        if (expectedPassword == null || !expectedPassword.equals(password)) {
            PrintWriter pw = resp.getWriter();
            pw.write("""
                    <h1>Login Failed</h1>
                    <p>Invalid username or password.</p>
                    <p><a href="/">Try again</a></p>
                    """);
            pw.close();
        } else {
            req.getSession().setAttribute("username", username);
            resp.sendRedirect("/");
        }
    }

如果账户名密码不匹配,则返回登录时报。 如果匹配,则通过HttpServletRequest获取到HttpSession 并设置HttpSession的属性req.getSession().setAttribute("username", username); 后续同一个username再访问系统页面时。 通过

HttpSession session = req.getSession(); String username = (String) session.getAttribute("username");

获取到的username非空时,则说明当先会话未过期,仍旧有效。

6、SessionManager中自动移除过期的Session逻辑。

    @Override
    public void run() {
        for (;;) {
            try {
                Thread.sleep(60_000L);
            } catch (InterruptedException e) {
                break;
            }
            long now = System.currentTimeMillis();
            for (String sessionId : sessions.keySet()) {
                HttpSession session = sessions.get(sessionId);
                if (session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000L < now) {
                    logger.warn("remove expired session: {}, last access time: {}", sessionId, DateUtils.formatDateTimeGMT(session.getLastAccessedTime()));
                    session.invalidate();
                }
            }
        }
    }

Thread.sleep(60_000L); 用来控制该清理逻辑每60s执行一次。 遍历当前的Session。 如果 当前时间 > session的 最大活跃间隔 时间。则从系统移除该Session。

maxInactiveInterval(session的最大活跃间隔 可以通过读取配置来进行初始化)