当前位置:数据分析 > Tomcat 中 Session 和 Cookie 又爱又恨

Tomcat 中 Session 和 Cookie 又爱又恨

  • 发布:2023-10-05 10:34

HTTP 是一种无状态通信协议。每个请求都是相互独立的,服务器无法识别之前的请求。对于 Web 应用程序,它们的活动依赖于某种状态,例如用户登录。这时使用HTTP就要求它有能力在一次登录请求后为后续请求提供登录信息。本文首发于公众号启蒙源码。 HTTP 是一种无状态通信协议。每个请求都是相互独立的,服务器无法识别之前的请求。对于 Web 应用程序,它们的活动依赖于某种状态,例如用户登录。这时使用HTTP就要求它有能力在一次登录请求后为后续请求提供登录信息。 解决方案是使用cookie,cookie由服务器返回给浏览器,浏览器在每次请求时缓存并提交cookie数据到服务器。Cookies在请求中以明文形式传输,大小限制为4KB。显然,将所有状态数据保存在浏览器中是不可靠的。主流的做法是: 当浏览器发出请求时,服务器为用户分配一个标识符,返回并存储在浏览器的 Cookies 中 服务器内部维护一个全局的请求状态库,并使用生成的标识符来关联每个请求的状态信息 浏览器发出的后续请求将标识符提交给服务器,以便从先前的请求中获取状态信息。 为了方便管理,服务器将整个过程称为会话,并抽象为Session类,用于识别和存储有关用户的信息或状态。 接下来我们通过Session标识符的解析和生成,Session的创建、销毁和持久化来分析Tomcat的源码实现。使用的版本是6.0.53。 1.解析会话标识符 Cookie 作为最常用的会话跟踪机制,所有 Servlet 容器都支持,Tomcat 也不例外。在Tomcat中,存储会话标识符的cookie的标准名称是JSESSIONID。 如果您的浏览器不支持cookies,您还可以使用以下方法记录标识符: URL重写:作为路径参数包含在url中,如/path;JSESSIONID=xxx URL请求参数:将session ID作为查询参数添加到页面所有链接中,如/path?JSESSIONID=xxx FORM隐藏字段:隐藏字段在表单中用于存储值,并与表单一起提交到服务器。Tomcat实现了从URL重写路径和cookie中提取JSESSIONID。在分析源码之前,先看一下设置cookie的响应和带cookie的请求的头域关键信息: // 设置 Cookie HTTP/1.1 200 OK 服务器:Apache-Coyote/1.1 Set-Cookie:JSESSIONID=56AE5B92C272EA4F5E0FBFEFE6936C91; Path=/examples Date: Sun, 12 May 2019 01:40:35 GMT // 提交 C cookie GET /examples/servlets /servlet/SessionExample HTTP/1.1 Host: localhost:8080 Cookie: JSESSIONID=56AE5B92C272EA4F5E0FBFEFE6936C91 1.1 从URL重写路径 包含Session ID路径参数的URL如下: http://localhost:8080/examples/SessionExample;JSESSIONID=1234;n=v/?x=x 简单来说,就是找到分号和斜杠之间匹配的JSESSIONID。这也是事实,但 Tomcat 是按字节操作的。核心代码在CoyoteAdapter.parsePathParameters()方法中,这里不再贴出。 1.2 来自Cookie头字段 触发Cookie解析的方法调用如下: CoyoteAdapter.service(请求、响应) └─CoyoteAdapter.postParseRequest(请求、请求、响应、响应) └─CoyoteAdapter.parseSessionCookiesId(请求、请求) └─Cookies.getCookieCount() └─Cookies.processCookies(MimeHeaders) └─Cookies .processCookieHeader(字节[], int, int)这个processCookieHeader对字节进行操作,并且解析看起来不直观。 Tomcat内部还有一个使用字符串解析的标记过时的方法,对理解很有帮助。代码如下: private void processCookieHeader(String cookieString){ // 多个cookie值,以逗号分隔 StringTokenizer tok = new StringTokenizer(cookieString, ";", false); while (tok.hasMoreTokens()) { String token = tok.nextToken(); // 获取等号的位置 int i = token.indexOf("="); if (i > -1) { // 获取名称和值并删除空格 String name = token.substring(0, i).trim( );字符串值 = token.substring(i+1, token.length()).trim(); // RFC 2109 和 bug 删除两端双引号 " value=stripQuote(value); // 从内部 cookie 缓存池获取 ServerCookie 对象 ServerCookie cookie = addCookie(); // 设置名称和值 cookie.getName(). setString(name); cookie.getValue().setString(value); } else { // 我们有一个坏饼干......就让它过去吧 } } }解析完成后,下一步就是在parseSessionCookiesId方法中遍历并尝试匹配名为JSESSIONID的cookie。如果存在,则将其值设置为Request的requestedSessionId,该Id与内部Session对象关联。 2. 生成会话cookie 与会话相关的 cookie 由 Tomcat 本身内部生成。当Servlet中使用Request.getSession()获取session对象时,会触发执行。核心代码:protected Session doGetSession(boolean create) { ... // 创建 Session 实例 if (connector.getEmptySessionPath() && isRequestedSessionIdFromCookie()) { // 如果会话 ID 来自 cookie,请重复使用该 ID。如果它来自 URL,请不要 // 重复使用会话 ID 以防止可能的网络钓鱼 session = manager.createSession(getRequestedSessionId()); } else { session = manager.createSession(null); } // 基于此会话 cookie 创建一个新会话 if ((session != null) && (getContext() != null) && getContext().getCookies()) { String scName = context.getSessionCookieName(); if (scName == null) { //默认 JSESSIONID scName = Glo bals.SESSION_COOKIE_NAME ; }   // 新 Cookie Cookie cookie = new Cookie(scName, session.getIdInternal()); // 设置路径域安全 configureSessionCookie(cookie); // 添加到响应头字段 response.addSessionCookieInternal(cookie, con) text.getUseHttpOnly() ); } if (session != null) { session.access();返回(会话); } else { 返回(空); } }将其添加到响应头字段中,就是根据Cookie对象生成开头描述的格式。 3.会议 Session是Tomcat内部的一个接口,是HttpSession的外观类。它用于维护 Web 应用程序特定用户的请求之间的状态信息。相关类图设计如下: 关键类或接口的功能如下: Manager - 管理Session池,不同的实现提供特定的功能,例如持久化和分发 ManagerBase - 实现了一些基本功能,例如Session池和ID生成算法,方便继承和扩展 StandardManager - 一种标准实现,可在该组件重新启动时提供简单的会话持久性(例如,当整个服务器关闭并重新启动时,或者重新加载特定 Web 应用程序时) PersistentManagerBase - 提供多种不同的持久存储管理方式,例如文件和数据库 存储 - 提供会话和用户信息的持久存储和加载 ClusterManager - 集群会话管理接口,负责会话复制方法 DeltaManager - 增量地将会话数据复制到集群中的所有成员 BackupManager - 仅将数据复制到一个备份节点,对集群的所有成员可见 本文不分析集群复制原理,只分析单机Session的管理。 3.1 创建会话 当在 Servlet 中使用 Request.getSession() 获取会话对象时,会创建一个 StandardSession 实例:public Session createSession(String sessionId) { // 默认返回的是新的 StandardSession(this) 实例 Session session = createEmptySession(); // 初始化属性 session.setNew(true); session.setValid(true); session.setCreationTime(System .currentTimeMillis()); //设置session有效时间,单位为秒,默认30分钟,负值表示永不过期 session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60); if (sessionId == null) { // 生成会话 ID sessionId =generateSessionId(); session.setId(sessionId);会话计数器++; SessionTiming计时 = new SessionTiming(session.getCreationTime(), 0);同步(sessionCreationTiming){ sessionCreationTiming.add(计时); sessionCreationTiming.poll();返回(会话); } 关键在于会话标识符的生成。我们看一下Tomcat的生成算法: 随机获取16字节 使用 MD5 加密这些字节,再次生成 16 字节数组 遍历新的字节数组,利用每个字节的高、低4位生成一个十六进制字符。 获取32位十六进制字符串 核心代码如下:protected StringgenerateSessionId() { byte random[] = new byte[16];字符串 jvmRoute = getJvmRoute();字符串结果=空; // 将结果渲染为十六进制数字字符串 StringBuffer buffer = new StringBuffer() ;执行 { int resultLenBytes = 0; if (result != null) { // 重复,重新生成缓冲区 = new StringBuffer();重复++; } // sessionIdLength 为 16 while (resultLenBytes < this.sessionIdLength) {       getRandomBytes(random);// 随机获取 16 个字节       // 获取这16个字节的摘要,默认使用 MD5       random = getDigest().digest(random);       // 遍历这个字节数组,生成一个32位的十六进制字符串       for (int j = 0;       j < random.length && resultLenBytes < this.sessionIdLength;       j++) {         // 使用指定字节的高低4位分别生成一个十六进制字符         byte b1 = (byte) ((random[j] & 0xf0) >> 4);字节 b2 = (字节) (随机[j] & 0x0f); // 转换为十六进制数字字符 if (b1 < 10) {buffer.append((char) ('0' + b1));} // 转换为大写十六进制字符 else {buffer.append((char) ('A ' + (b1 - 10)));} if (b2 < 10) {buffer.append((char) ('0' + b2 )));} else {buffer.append((char) ('A' + ( b2 - 10)));} 结果LenBytes++; if (jvmRoute != null) {buffer.append('.').append( jvmRoute);} 结果 = buffer.toString(); while (sessions.containsKey(结果));返回(结果); } 3.2 会话过期检查一个Web应用程序对应一个会话管理器,这意味着StandardContext内部有一个Manager实例。每个容器组件都会启动一个后台线程,并定期调用自身和内部组件的backgroundProcess()方法。 Manager后台处理是检查Session是否过期。 检查的逻辑是获取所有的session,通过它们的isValid来判断是否过期。代码如下: public boolean isValid() { ... // 是否检查是否活跃,默认 false if (ACTIVITY_CHECK && accessCount.get() > 0) { return true; } } // 检查时间是否已过期 if (maxInactiveInterval >= 0) { long timeNow = System.currentTimeMillis(); int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); if (timeIdle >= maxInactiveInterval) { // 如果过期了,进行一些内部处理 // 主要是通知过期事件 感兴趣的监听者 expire(true); } } // 复数永不过期 return (this.isValid); } 3.3 会话保持 持久化就是将内存中活跃的Session对象序列化到文件或者存储到数据库中。如果会话管理组件符合并启用了持久化,则会在其生命周期事件的stop方法中进行存储;加载将在start方法中执行。 对文件的持久化。 StandardManager还提供了文件持久化的功能。它将会话池中的所有活动会话写入 CATALINA_HOME/work/Catalina///SESSIONS.ser 文件。代码位于其 doUnload 方法中。 。FileStore还提供了持久化文件的功能。与 StandardManager 的区别在于它将每个会话写入一个名为 .session 的文件。 要持久保存到数据库,请使用以下 SQL 将会话相关数据存储在表中,包括序列化的二进制数据。表字段信息如下: 创建表 tomcat_sessions ( session_id varchar(100) 不为空主键、 valid_session char(1) 不为空、 max_inactive int 不为空、 last_access bigint 不为空、 app_name varchar(255)、 Session_datamediumblob、 KEY kapp_name(app_name)); 注意:数据库驱动的jar文件需要放在$CATALINA_HOME/lib目录下,才能让Tomcat内部的类加载器可见。 4. 总结 本文简单分析Tomcat对Session的管理。当然,很多细节都被忽略了。有兴趣的可以深入源码。 Tomcat集群Session的实现稍后分析。

相关文章

最新资讯