基于session的登录流程
session的登录流程图
1. 发送验证码
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户
2. 短信验证码登录、注册
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息
3. 校验登录状态
用户在请求的时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并放行
登录拦截
为什么需要登录拦截
当执行不同的业务,访问各个controller层时,不可能在每个controller里面写检验登录状态的业务逻辑,在spring mvc中有拦截器,所有请求先经过拦截器,由拦截器决定是否需要放行,到达controller。这样校验只需要一次。在拦截器里获取到的用户信息通过threadlocal传递到controller。
隐藏用户敏感信息
用户信息若是直接返回,会得到许多我们不需要的信息。
所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏,采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了
session共享问题
每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?
早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了
但是这种方案具有两个大问题
- 每台服务器中都有完整的一份session数据,服务器压力过大。
- session拷贝数据时,可能会出现延迟
所以我们后面都是基于Redis来完成,我们把session换成Redis,Redis数据本身就是共享的,就可以避免session共享的问题了
Redis替代session的登录流程
设计key结构
首先我们来思考一下该用什么数据结构来存储数据
由于存入的数据比较简单,我们可以使用String或者Hash
如果使用String,以JSON字符串来保存数据,会额外占用部分空间
如果使用Hash,则它的value中只会存储数据本身
如果不是特别在意内存,直接使用String就好了
设计key的具体细节
我们这里就采用的是简单的K-V键值对方式
但是对于key的处理,不能像session一样用phone或code来当做key
因为Redis的key是共享的,code可能会重复,phone这种敏感字段也不适合存储到Redis中
在设计key的时候,我们需要满足两点
key要有唯一性
key要方便携带
所以我们在后台随机生成一个token,然后让前端带着这个token就能完成我们的业务逻辑了
整体访问流程
当注册完成后,用户去登录,然后校验用户提交的手机号/邮箱和验证码是否一致
如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到Redis,并生成一个token作为Redis的key
当我们校验用户是否登录时,回去携带着token进行访问,从Redis中获取token对应的value,判断是否存在这个数据
如果不存在,则拦截
如果存在,则将其用户信息(userDto)保存到threadLocal中,并放行
登录刷新问题
什么是登录状态刷新问题
我们依赖于拦截器做登录校验,需求是只要用户一直访问,token有效期就一直刷新,不会过期,但是我们拦截器拦截的路径只是需要做登录校验的路径,并不是所有路径,一个服务中存在不需要登录校验的操作(如首页等),如果用户进行不需要登录校验的请求,token的有效期不会刷新。
优化方案
类似责任链模式的思想,在原有拦截器的基础上,加一个拦截器,拦截一切路径,在第一个拦截器中做刷新token有效期的操作。
既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。