最近在学习黑马点评项目的Redis使用,原本学习过程跳过了JavaWeb的学习过程,所以登录校验这部分知识几乎没有,所以借着这个机会重新学习一个登录验证的过程。似乎现在使用jwt令牌验证登录更加流行,以后还需要进行补充。
基础知识:
一、什么是 Session?
HTTP 是“无状态”协议
浏览器和服务器每一次请求/响应,默认互不“记忆”彼此信息。
也就是说,服务器无法知道这一次请求和上一次请求是同一个用户发过来的。
Session 的作用
“会话”(Session)是服务器为每个用户专门开辟的一块存储空间,用来在多次请求之间保存用户的数据。
当用户第一次访问网站时,服务器会为他创建一个 session 对象,并给浏览器返回一个 Session ID(通常使用 Cookie 存放)。
浏览器后续请求都会带上这个 ID,服务器通过 ID 找回对应的 session,从而记住用户状态。
二、session.setAttribute
是什么?
在 Java Web(Servlet)中,HttpSession
接口代表一个用户的会话。它提供了以下主要方法来操作会话数据:
setAttribute(String name, Object value)
把一个叫做name
的“名字”与value
(任意对象)绑定到当前 session 上。getAttribute(String name)
取出之前存过的name
所对应的对象。removeAttribute(String name)
删除这个名字对应的数据。
java
复制编辑
// 举例:在登录成功后,把当前用户的信息存到 session 里 UserDTO user = ...; // 你查到的用户对象 request.getSession().setAttribute("loginUser", user);
这行代码的意思是:
名字(key):
"loginUser"
值(value):
user
对象把它们放到服务器内存中的当前用户的 session 里。
三、setAttribute
存储的到底是什么?
键值对
就是一个
Map<String, Object>
:名字(键)对应一个对象(值)。
存储位置
存在 服务器内存(或集群时存在多个节点的共享存储)里,不在浏览器或网络上,浏览器只保存一个 ID。
值可以是任意对象
Java 中任何继承自
java.io.Serializable
的对象都可以放;也可以是普通的 POJO(普通 Java 对象),但如果做集群或持久化,需要序列化。
四、容量限制
没有固定“上限”
Session 本质上就是放在内存里的一个
Map
,它能装多少,主要取决于服务器的内存大小。但现实中你不应该把“超大”对象放进去,否则会耗尽内存,导致服务器 OOM(内存溢出)。
实际建议
一般存一些轻量级数据:
用户 ID、用户名、简单权限列表、购物车中商品 ID 列表……
不要把图片、视频、超大列表、数据库连接等“重量级”对象直接放到 session。
集群或持久化
如果你把 session 配置为“持久化”或“分布式”,session 中的对象会被序列化并写到磁盘、数据库或分发到其他节点。
这时可序列化对象越大,序列化和网络传输的开销也越大,会影响性能。
Session登录流程:
一、验证码发送阶段:
用户在客户端输入手机号phone,点击获取验证码
->
前端接收phone,访问 发送验证码接口: user/code
----------------------------------------------
注:******************************************
后端服务器此时接收前端的请求,前后端第一次交互
后端服务器生成session并返回给前端浏览器sessionid
此刻到用户关闭浏览器为止,或session周期结束前
用户的sessionid固定在cookie中,之后前端的请求
都会携带sessionid,后端可以选择读与不读
注:******************************************
----------------------------------------------
->
1.后端获取phone,session后生成随机验证码code,
2.后端保存code,这里使用session.setAtrribute()
将code保存在session中(用于与前端code匹配实现登录)
3.通过云技术或其他将生成的code直接发送给用户
二、验证码校验阶段
用户输入验证码code,访问 登录接口 user/login
->
后端接收到来自前端的DTO对象以及code
1.后端读取session中的cacheCode
2.if cacheCode = code
3.no -> 报异常
4.yes -> 根据DTO对象查询数据库->得到user
4.1 user = null -> 创建user
4.2 将user对象信息存入session中
->返回前端code:200
三、鉴权
由于目前网页采用http\https协议,而该协议是无状态的,所以登陆后很多页面需要重复进行鉴权后才能访问,这么多页面如果都需要重复登录(也就是不登陆怎么鉴权的问题),代码实现很麻烦。
所以考虑springmvc中的拦截器,从而将页面分配成
1.不需要登录就能访问的内容
2.需要登录才能访问的内容
将鉴权所需的信息存放在session中,从而避免登录才能访问的页面需要反复登陆的繁琐过程。
1.创建拦截器Interceptor
->
1.1在拦截器中获取前端传递的reqeust
-----------------------------------------------------------------
*****************************************************************
前置增强
->
1.2requet中获取session
(这里我猜测是后端服务器自行根据sessionId判断读取对应的session)
->
1.3从session中获取user对象
->
1.4.1user = null ->return false进行拦截
1.4.2user != null
->
1.5将user信息通过ThreadLocal存储在线程中
后续的访问只需要读取ThreadLocal的信息
前置增强
-----------------------------------------------------------------
*****************************************************************
-----------------------------------------------------------------
*****************************************************************
后置增强
1.6在对应接口实现结束后回收LocalThread
后置增强
-----------------------------------------------------------------
*****************************************************************
->
2.将拦截器配置在mvcConfig进行触发
3.在mvcConfig使用execludePathPatern来标记不需要鉴权的页面
最典型的就是登录页面
登陆页面需要登录后才能访问,这个逻辑本身就是错的
基于Redis和token登录流程:
1.在上述内容中简单描述了基于session方式实现验证码登录的形式,但是session很快就会面临问题:
比如session的内容是保存在后端服务器中,比如tomcat。
由于现在使用nginx服务器,既然使用了nginx服务器,那么使用荷载均衡就是很正常的一件事情,
nginx会通过反向代理的方式管理多个后端服务器,
这就会造成第一次用户访问页面使用的是服务器A,而第二次访问页面可能使用的就是服务器B了。
刚才又说session实质上存储在服务器中的,那么服务器A与服务器B之间的session并非共享的。
刚才还说session其实保存的是我们的鉴权信息,那么用户最开始的鉴权信息保存在服务器A,后续访问被nginx反向代理却去访问服务器B了,那么服务器B中显然是没有最初的session的。
所以这里就出现了矛盾,当然tomcat本身能够处理这样的问题,但这里采取更加简单实用的方式:
也就是基于REDIS与TOKEN来实现验证码登录流程。
2.token的定义个人简单理解为:
前端在请求头中保存一个键值对用于存储token,如:token_name : token_value
token_value实际由后端随机创建,比如简单使用UUID来随机创建
3.流程
该流程与session登录流程大差不差,以下简单描述两个登陆方式之间的区别(为简化描述session登录称为A,redis-token登录称为B):
3.1 首先A的登录时将code与user对象都存入至session,而B将code存放进redis中作为缓存,并且设定具体时间销毁(防止redis压力爆了)。
而B在验证code时,通过存放code的键值对来从后端获取code,与通过云发送给用户的code进行比对。
B在存放user的时也与A不同,A将user对象存放在session中,而B使用token_value作为redis中的key,而将user作为value。
匹配时,从header中使用token-name(前后端都知道的名称)拿到token-value(后端随机出来的字符串),又因为redis的键值对形式为key:token-value value:user对象
从而根据token-value拿到user对象,最后将user对象存放在ThreadLocal中。
3.2A登录时鉴权方式在拦截器中通过request拿到session,再通过session去拿存在域中的code与user,而B通过拦截器中的request拿到请求头header,再通过请求头拿到对应的token-value。
其余内容基本相同,使用redis显然缓解了后端服务器的管理压力,将压力分散给redis,同时redis集群能够很好解决tomcat多台服务器无法获取session的尴尬。