【声明】文章为本人学习时记录的笔记。
原课程地址: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应用调用HttpServletRequest
的getSession()
方法时,需要返回一个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
通过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);
接口上的解释也很清楚:
如果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 = """
Index Page
Please Login
User Name:
Password:
Login
""";
} else {
html = """
Index Page
Welcome, {username}!
Logout
""".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("""
Login Failed
Invalid username or password.
Try again
""");
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的最大活跃间隔 可以通过读取配置来进行初始化)