导入代码:
遇到问题:
jdk版本问题:将编译和运行版本全都改为jdk 17
Redis连接不上:host和port要改为自己本地Redis的host和port,且Windows下的Redis没有密码,需要在配置文件中注释掉

服务启动后循环报错:因为刚导入时Redis消息队列还没有写好,service/VoucherOrderServiceImpl的run()方法会一直循环执行读取消息队列的消息,所以报错,这里需要改一个地方,让任务先不提交

资料提供的sql代码没有建立数据库语句:首先要create一个数据库hmdp,然后再运行sql文件
运行sql代码时报错:将这段代码中的begin_time和end_time两行的DEFAULT后面的默认值改为CURRENT_TIMESTAMP(MySQL8后不支持原来的写法)

运行代码:
启动后端:成功运行后端Springboot服务后,在浏览器中输入http://localhost:8081/shop-type/list,如果能够获取到网页数据,则说明后端配置成功了

启动前端:将资料中的nginx解压到黑马点评文件夹下,并运行里面的nginx.exe,然后按F12打开开发者工具将浏览器切换到手机形式,在浏览器中输入http://localhost:8080,如果出现下面页面,则说明成功

基于Session实现登录:

发送短信验证码功能:请求方式POST,路径/user/code
Controller层:UserController下的sendCode()方法

Service层:UserServiceImpl下的sendCode()方法,首先要使用RegexUtils类下的isPhoneInValid()方法利用正则表达式校验手机号格式,如果符合格式就生成验证码,并将验证码保存到session中,最后发送验证码

实现短信验证码登录和注册功能:请求方式POST,请求路径/user/login
Controller层:UserController下的login()方法

Service层:UserServiceImpl下的login()方法,首先要校验手机号,然后从session中获取到验证码并校验;如果都一致,则根据手机号查询用户;如果用户存在,则将用户信息保存到session中,如果不存在则创建新用户并保存到session中,最后返回一个null


方法createUserWithPhone():

上面的方法中,query()和save()方法都是用到了mybatis-plus的接口,直接对数据库进行操作
登录验证功能:
获取当前登录用户:UserController下的me()方法,使用UserHolder类来获取当前线程下的用户

登录拦截器:utils/LoginInterceptor,里面有两个方法preHandle()和afterCompletion(),一个是用于拦截用户并检查用户是否存在的,另一个是用来移除用户的


配置整个项目的拦截器:config/MvcConfig类,里面定义了一个addInterceptors()方法,调用了我们的LoginInterceptor()作为拦截器,并定义了排除的路径

隐藏用户敏感信息:用户登录方法login()中,查询到用户User之后就直接将其保存进session,导致用户的敏感信息都保存进去了。可以用UserDTO类来表示User的非敏感信息,使用BeanUtils.copyProperties()方法将User类的属性转移到UserDTO类中,再保存进session

此时就需要改LoginInterceptor类中的代码:

这样前端通过/user/me路径获取到的用户信息就只有UserDTO中的属性

Session共享的问题分析:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失
session的替代方案应该满足:
- 数据共享
- 内存存储
- key,value结构

基于Redis实现共享session登录:

Redis中保存的key是用户的手机号,格式为phone: xxx,value是验证码
保存登录的用户信息时,可以使用String结构,以JSON字符串来保存,比较直观:

然而,hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少:

用户数据的key我们用一个随机token来表示,value为一个hash表。由于之前的校验登录状态是基于cookie的session id来获取用户信息,现在不用session了,就需要基于token从redis中获取信息,此时需要服务端将用户保存时生成的token返回给客户端,这样客户端才能携带token从Redis中获取用户信息

代码修改:
发送验证码:在UserServiceImpl的sendCode()方法里将保存验证码到session改为保存验证码到Redis

注意存入Redis的验证码要设置一个有效期,第三个参数为有效期数据,第四个参数为单位
登录功能:在UserServiceImpl的login()方法里将获取验证码和保存用户都改为对Redis操作,其中保存用户需要为每一个用户生成一个token作为key,并将User对象转换为Hashmap作为value,最后将key和value存入Redis中并设置token有效期。最后服务端需要将这个token返回给客户端,以便于之后的校验登录状态。



对LoginInterceptor类,需要更改它的登录校验功能,所以需要用到StringRedisTemplate。但是LoginInterceptor不属于Spring容器,所以不能使用@Autowired注解来自动注入。这时候需要用到配置拦截器的MvcConfig类,它在addInterceptors()方法中初始化了LoginInterceptor作为拦截器,所以可以利用有参构造器,在构造时将StringRedisTemplate作为参数注入到LoginInterceptor中。
MvcConfig:添加自动注入的StringRedisTemplate,并在构造LoginInterceptor时作为参数


LoginInterceptor:定义一个private的StringRedisTemplate,并添加一个有参构造器

基于上面所改进的内容,最终完成的登录拦截代码如下所示:首先从请求头中获取token,并基于token获取redis中的用户,此时value为map对象,还要将map对象转换为UserDTO对象;并保存用户信息到ThreadLocal,最后刷新token有效期并放行。

Redis代替session需要考虑的问题:
- 选择合适的数据结构
- 选择合适的key
- 选择合适的存储粒度
解决状态登录刷新的问题:
登录拦截器存在问题:之前的拦截器只拦截需要登录的路径,对不需要登录的路径比如网站首页是不会拦截的,此时如果用户只在这些页面进行浏览,则token有效期不会刷新,则会出现用户还在活跃,却退出登录的情况。这时我们可以对一切路径添加另一个拦截器,在这个拦截器中进行token的刷新,并在另一个登录拦截器中对当前登录的用户是否存在进行判断,如下图所示:

注意:在第一个拦截器获取token后,如果token不存在,则说明用户没有登录,但是可以以访客身份进行浏览,所以可以直接跳过234步并放行。刷新token有效期仅对登录的用户生效。
代码实现:新增一个拦截器RefreshTokenInterceptor,作为第一个拦截器,其中preHandle()方法如下:在用户不存在时,需要直接放行而不是拦截

LoginInterceptor的preHandle代码中关于刷新token有效期的代码也可以直接删掉,这个拦截器只判断当前是否有用户登录,如果没有则拦截;另外,这个类中的有参构造器和afterCompletion()这个方法也要删掉,将其复制粘贴到RefreshTokenInterceptor中

最后,将配置拦截器的MvcConfig中的addInterceptors()方法进行修改:增加一个RefreshTokenInterceptor,并通过order()方法设置两个拦截器的顺序




