【Java Web编程 九】深入理解会话追踪技术Session和Cookie(下)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 【Java Web编程 九】深入理解会话追踪技术Session和Cookie

Session基本概念

服务器第一次接收到请求时,开辟了一块 Session 空间(创建了Session对象),同时生成一个 sessionId ,并通过响应头的 Set-Cookie:JSESSIONID=XXXXXXX 命令,向客户端发送要求设置 Cookie 的响应; 客户端收到响应后,在本机客户端设置了一个 JSESSIONID=XXXXXXX 的 Cookie 信息,该 Cookie 的过期时间为浏览器会话结束

接下来客户端每次向同一个网站发送请求时,请求头都会带上该 Cookie信息(包含 sessionId ), 然后,服务器通过读取请求头中的 Cookie 信息,获取名称为 JSESSIONID 的值,得到此次请求的 sessionId。

Session生命周期

Session存储在服务器端,一般放置在服务器的内存中(为了高速存取),Session在用户访问第一次访问服务器时创建,需要注意只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session,可调用request.getSession(true)强制生成Session。Session什么时候失效?

  1. 服务器会把长时间没有活动的Session从服务器内存中清除,此时Session便失效。Tomcat中Session的默认失效时间为30分钟。
  2. 调用Session的invalidate方法。

Session对浏览器的要求:虽然Session保存在服务器,对客户端是透明的,它的正常运行仍然需要客户端浏览器的支持。这是因为Session需要使用Cookie作为识别标志。HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为JSESSIONID的Cookie,它的值为该Session的id(也就是HttpSession.getId()的返回值)。Session依据该Cookie中的JSESSIONID来识别是否为同一用户

该Cookie为服务器自动生成的,它的maxAge属性一般为-1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。

为什么session只能显示在一个浏览器,根源就在cookie,关闭浏览器的时候,会话cookie被关闭,sessionid被丢失,再次访问的时候由于找不到sessionid只能创建新的sessionid,因此同一机器的两个浏览器窗口访问服务器时,会生成两个不同的Session。但是由浏览器窗口内的链接、脚本等打开的新窗口除外。这类子窗口(在链接上右击,在弹出的快捷菜单中选择在新窗口中打"时,子窗口便可以访问父窗口的Session)会共享父窗口的Cookie,因此会共享一个Session,以下是一个子窗口的示例:

所以说Session并没有被销毁,只是我们打开另一个浏览器使用的是一个新的session,我们用新的session而不用旧的,相当于抛弃了它,实际上它还默默的在服务器端活30分钟。相反request和response每次请求后确确实实被销毁了。

Session常用方法

HttpServletRequest 接口中提供了两个方法来创建 HttpSession 实例,并进行一些简单的操作,通过看Session的源码可以看出:

public interface HttpSession {
    long getCreationTime();
    String getId();
    long getLastAccessedTime();
    ServletContext getServletContext();
    void setMaxInactiveInterval(int var1); //设置 Session 的有效时间
    int getMaxInactiveInterval();
    /** @deprecated */
    @Deprecated
    HttpSessionContext getSessionContext();
    Object getAttribute(String var1);
    /** @deprecated */
    @Deprecated
    Object getValue(String var1);
    Enumeration<String> getAttributeNames();
    /** @deprecated */
    @Deprecated
    String[] getValueNames();
    void setAttribute(String var1, Object var2);
    /** @deprecated */
    @Deprecated
    void putValue(String var1, Object var2);
    void removeAttribute(String var1);
    /** @deprecated */
    @Deprecated
    void removeValue(String var1);
    void invalidate();
    boolean isNew();

Session的缺点

虽然Session很好的避免了Cookie的通信复杂度,但是对于像京东天猫这样服务器就需要记录上亿个session id,这对服务器来说显然不友好,因为线上为了应对大量请求,我们一定是使用服务器集群去分散处理请求的,那么如果用户UA第一次请求了服务器F1,生成了session id,下次请求被路由到了服务器F2,而F2上没有UA的。

  1. 这个时候服务器说,那我这样吧,我把用户请求的服务器记录下来,粘住特定的session id请求到特定的服务器上,这就是session 粘连策略

这样也有风险,万一特定的这个服务器挂了,这个服务器上记录的session就都丢失了,某些特定用户就永远登录不上了

  1. 这个时候服务器说,那这样吧,只要有session产生就复制到各个服务器上,这样就有保障了,这就是session的复制策略

但是这样复制来复制去太麻烦,太耗费性能了,如果有一个统一的机器负责管理session就好了,而且这个统一的机器最好是个集群,这样防止单点挂掉的风险。

  1. 这样就引入了中间件集群这种方式去解决问题,例如引入Redis集群去处理,这就是session 共享策略

但是这样仍然将session置于一个不确定不稳定的状态,因为第三方的集群也可能有宕机的危险

Token机制

那么如果服务端不对session进行记录,只是验证会话有效性呢?也就是客户端第一次请求后服务端依据用户数据计算出一个token签名然后返回给客户端,客户端第二次请求的时候把数据和token签名一起发来,服务端再用相同算法对数据进行签名,得出的token签名一致即认证通过。这就是token的概念

Token解决购物车问题

继续延续上文的购物车问题,有了token之后可以不必考虑session的存储问题,分布式集群问题,按照如下图的流程,我们在

从上图中可以看出,服务端不存储任何内容,token 只存储在浏览器中,服务端却没有存储,server 会有一套校验机制,校验这个 token 是否合法。那么token既然没有存储到服务端,不像 session 那样根据 sessionId 找到 userid,怎么定位请求的用户呢? 其实token 本身携带 uid 信息。

当然其实我们可以看的出,Token并没有在服务端存储数据,那么怎么记录购物车信息呢,其实Token是一种轻量级的用户认证方案,只能认证当前用户登录的有效性,而并不能追踪用户行为,所以可以说Token在用户验证领域很专业,但是用途比较单一,如果要解决这里的购物车问题,还需要token结合一些数据存储技术,例如数据库或者redis,token负责验证当前用户,数据库或者redis负责进行购物车存取操作。

Token的基本概念

token 验证是无状态的,服务器不记录哪些用户登录了或者哪些 JWT 被发布了,而是每个请求都带上了服务器需要验证的 token,token 放在了 Authorization header 中,形式是 Bearer { JWT },但是也可以在 post body 里发送,甚至作为 query parameter。JWT的结构如下;

可以看到 token 主要由三部分组成:

  • header:指定了签名算法。
  • payload:可以指定用户 id,过期时间等非敏感数据。
  • Signature:签名,server 根据 header 知道它该用哪种签名算法,再用密钥根据此签名算法对 head+payload 生成签名,这样一个 token 就生成了。

当 server 收到浏览器传过来的 token 时,它会首先取出 token 中的 header+payload,根据密钥生成签名,然后再与 token 中的签名比对,如果成功则说明签名是合法的,即 token 是合法的。而且你会发现 payload 中存有我们的 userId,所以拿到 token 后直接在 payload 中就可获取 userid,避免了像 session 那样要从 redis 去取的开销。

整体的验证流程如下:

  1. 用户输入登录信息
  2. 服务器判断登录信息正确,返回一个 token
  3. token 存储在客户端,大多数通常在 local storage,但是也可以存储在 session storage 或者 cookie 中。
  4. 接着发起请求的时候将 token 放进 Authorization header,或者同样可以通过上面的方式。
  5. 服务器端解码 JWT 然后验证 token,如果 token 有效,则处理该请求。
  6. 一旦用户登出,token 在客户端被销毁,不需要经过服务器端。

可以看的出,token就是一个令牌的概念。

Token的缺点

既然 token 这么好,那为什么我们依然大多数使用场景还是Session?token也有它自身的缺点:

  1. token 太长了,token 是 header,payload 编码后的样式,所以一般要比 sessionId 长很多,很有可能超出 cookie 的大小限制(cookie 一般有大小限制的,如 4kb)。如果你在 token 中存储的信息越长,那么 token 本身也会越长,这样的话由于你每次请求都会带上 token,对请求来是个不小的负担。
  2. token不太安全,我们说 token 是存在浏览器的,既然它太长放在 cookie 里可能导致 cookie 超限,那就只好放在 local storage 里。这样会造成安全隐患,因为 local storage 这类的本地存储是可以被 JS 直接读取的。

所以 token 更适合一次性的命令认证,设置一个比较短的有效期

应用场景的划分

详细的比较下Session、Cookie以及Token的区别和应用场景。

Session和Cookie的使用场景区别

Cookie 和 Session都是⽤来跟踪浏览器⽤户身份的会话⽅式,但是两者的应⽤场景不太⼀样。

  • Cookie⼀般⽤来保存⽤户信息,通过这种方式不需要用户反复去输入用户信息去登录
  • 我们在 Cookie 中保存已经登录过得⽤户信息,下次访问⽹站的时候⻚⾯可以⾃动帮你登录的⼀些基本信息给填了;
  • ⼀般的⽹站都会有保持登录也就是说下次你再访问⽹站的时候就不需要重新登录了,这是因为⽤户登录的时候我们可以存放了⼀个 Token 在 Cookie中,下次登录的时候只需要根据 Token 值来查找⽤户即可(为了安全考虑,重新登录⼀般要将 Token重写);
  • 登录⼀次⽹站后访问⽹站其他⻚⾯不需要重新登录
  • Session 的主要作⽤就是通过服务端记录⽤户的状态。 典型的场景是购物⻋,当你要添加商品到购物⻋的时候,系统不知道是哪个⽤户操作的,因为 HTTP 协议是⽆状态的。服务端给特定的⽤户创建特定的 Session 之后就可以标识这个⽤户并且跟踪这个⽤户了。
  • Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。Cookie 存储在客户端中,⽽Session存储在服务器上,相对来说 Session 安全性更⾼。如果要在Cookie 中存储⼀些敏感信息,不要直接写⼊ Cookie 中,最好能将 Cookie 信息加密然后使⽤到的时候再去服务器端解密

可以通俗的这么理解,Cookie在客户端保存用户信息,知道用户干了什么事儿,Session则在服务端给用户创建了相应的数据存档,下次只要对应用户登录就能读档,那么要想找到用户正确的访问内容需要二位配合了。

JWT 和 Session+Cookies 基于身份验证的区别

JWT 和 Session Cookies 都提供安全的用户身份验证,但是它们有以下几点不同

  • 密码签名,JWT 具有加密签名,而 Session Cookies 则没有。
  • 连接状态:JWT 是无状态的,因为声明被存储在客户端,而不是服务端内存中。身份验证可以在本地进行,而不是在请求必须通过服务器数据库或类似位置中进行。 这意味着可以对用户进行多次身份验证,而无需与站点或应用程序的数据库进行通信,也无需在此过程中消耗大量资源。
  • 可扩展性,Session Cookies 是存储在服务器内存中,这就意味着如果网站或者应用很大的情况下会耗费大量的资源。由于 JWT 是无状态的,在许多情况下,它们可以节省服务器资源。因此 JWT 要比 Session Cookies 具有更强的可扩展性。
  • 是否可跨域,JWT 支持跨域认证,Session Cookies 只能用在单个节点的域或者它的子域中有效。如果它们尝试通过第三个节点访问,就会被禁止。如果你希望自己的网站和其他站点建立安全连接时,这是一个问题。使用 JWT 可以解决这个问题,使用 JWT 能够通过多个节点进行用户认证,也就是我们常说的跨域认证。

需要说明的是,JWT仅仅是一种用户身份认证的解决方案,如果我们进行购物车的设置,JWT搞不定。

总结一下

这篇Blog参照了很多内容,共写了两天,有很多地方也很纠结没有想通,现在进行一下思路整理吧:

  • cookie就是由服务端生成返回给客户端的,并且在第一次生成时默认给放置了一个Jsession id
  • 浏览器请求服务端时,通过Jsessionid找到对应的session,然后进行对应存取操作,这个查找过程是tomcat帮我们做的,我们在代码里不需要关心,也就是说我们不用调用什么通过sessionid获取session对象之类的方法。
  • 一个客户端浏览器对应一个目标站点【localhost】只产生一个session、一个cookie,我们追踪用户会话,前提也是在这个范围内讨论的,如何跨域生效、如何跨浏览器记忆之类的问题不在讨论范围内,我们只专注于当前客户端浏览器对于当前目标站点是如何进行会话追踪的
  • JSP中的内置对象session就是和Servle中的这个HttpSession生成的,所以之前我们讨论JSP的作用域时其实也大概提了下,只是当时还不知道为什么session的作用域是一个浏览器,并且为什么能跨请求,现在我们知道了,是因为整个浏览器请求目标站点范围内,用的sessionid是同一个,如果cookie一直存在,那么HttpSession就还是那一个,所以作用范围才是这么大。而request只不过是doGet的一个局部变量,当然会请求一次后作用域失效。
  • 对于用户身份验证这种场景,最好用token,因为session操作起来太复杂,没必要为了验证用户身份搭建一套redis,而如果有其它诉求,例如追踪用户的一些信息,还是要使用cookie+session,因为token不在服务端存储数据。
  • cookie+session这种组合用途也要细分,cookie由于存储在客户端本地,所以最好不要放一些敏感信息,而cookie由于存放在客户端本地,所以可以存放很长时间,所以cookie适合防止要求长期存储但不敏感的数据,而session由于运行在服务器内存中或者redis集群中,而且通常有个过期时间,所以适合存放一些短期但重要的数据
  • 当禁用cookie时,需要URL重写技术对Session进行跟踪,这里不做更多讨论
  • 隐藏表单域提交也属于一种会话追踪技术,这里也不做更多的讨论

本篇Blog提到的购物车的例子,由于购物车的信息在各大电商网站比较重要,所以一般会落库并进行数据分析,所以不一定是存储在session或者cookie中,这里只是举个例子,具体以业务场景为准。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
13天前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
18 2
|
8天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
15天前
|
Java API Apache
Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
【10月更文挑战第29天】Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
71 5
|
10天前
|
安全 Java 编译器
JDK 10中的局部变量类型推断:Java编程的简化与革新
JDK 10引入的局部变量类型推断通过`var`关键字简化了代码编写,提高了可读性。编译器根据初始化表达式自动推断变量类型,减少了冗长的类型声明。虽然带来了诸多优点,但也有一些限制,如只能用于局部变量声明,并需立即初始化。这一特性使Java更接近动态类型语言,增强了灵活性和易用性。
92 53
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
6天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
8天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
22 2
|
9天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
10天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
37 1
|
13天前
|
存储 安全 搜索推荐
理解Session和Cookie:Java Web开发中的用户状态管理
理解Session和Cookie:Java Web开发中的用户状态管理
37 4