黑马点评项目-短信登录功能

一、导入黑马点评项目

1、代码下载

视频资源链接:P25 实战篇-02.短信登录-导入黑马点评项目
代码可以直接去黑马微信公众号上搜索,或者从下面的网盘链接中下载:链接:
https://pan.baidu.com/s/1aWhWVn2Ai7AeuDm0KftSqw
提取码: snuw

2、数据库

2.1 SQL 脚本执行

上面的链接中有一个 hmdp.sql 文件,注意在执行该 SQL 脚本的时候不要批量执行,会报错,最好是一条一条执行。

2.2 数据库表介绍

  • tb_user:用户表
  • tb_user_info:用户详情表
  • tb_shop:商户信息表
  • tb_shop_type:商户类型表
  • tb_blog:用户日记表(达人探店日记)
  • tb_follow:用户关注表
  • tb_voucher:优惠券表
  • tb_voucher_order:优惠券的订单表

3、前端 Nginx 搭建

3.1 Mac OS 安装 Nginx

我自己用的是 Mac 电脑,原本是在 Mac 上安装了 Nginx,安装教程可以参考这位大佬写的 mac安装nginx
安装完成后,执行brew info nginx
在这里插入图片描述
注意这个服务目录,就是用来放置黑马点评项目的前端项目的。
在这里插入图片描述
配置目录中的nginx.conf,要替换成黑马点评前端项目中的 nginx.conf,当然如果不嫌麻烦或者自己对 Nginx 比较了解,也可以自己配置。
黑马点评前端项目 nginx.conf 的位置如下图所示:nginx-1.18.0/conf/nginx.conf
在这里插入图片描述
替换下面的文件,一般替换前最好是将源文件做个备份,此处可以不用备份,官方已经给我们备份好了:nginx.conf.default
在这里插入图片描述

3.2 虚拟机安装 Nginx

但是吧,这样有个问题,就是我每次关机重启后,nginx 都需要重新启动一遍,所以,我就又在虚拟机上安装了一下 Nginx,安装教程可以参考 centOS7安装nginx及nginx配置。当然也可以上网搜一搜如何将 Nginx 设置为开机自启动,这样这一步也就不用了(我已经尝试了各种方式,均以失败告终,我放弃了)。
安装完成后,也需要将黑马点评前端项目放置到服务目录,将配置目录中的 nginx.conf 替换成黑马点评前端项目的 nginx.conf。
替换过程如下:

1、先将黑马点评前端项目压缩成 hmdp.tar.gz

我是直接放在了桌面,放在哪了,下面的目录就替换成哪

tar -zcvf hmdp.tar.gz [指定压缩文件存放位置] /Users/Mac用户名/Desktop/资料/nginx-1.18.0/html/hmdp

2、将压缩包上传至虚拟机

要注意一点,如果在上述命令中未指定压缩文件存放位置,那么文件压缩后存放的位置就是自己当前所在目录,由于我没指定,最终文件生成的位置,如下图所示,就是 scp 命令后面的那个地址。
知道文件压缩后在哪了,就将文件上传至虚拟机。通过 scp 命令,Mac 可以通过终端命令直接将文件上传至虚拟机(或者服务器)。
在这里插入图片描述

再来是配置文件的替换:
在这里插入图片描述

3、登录虚拟机,解压上传的压缩包

进入到压缩包所在的目录,执行解压命令:

cd /usr/local/nginx/html   ## 进入到压缩包所在的目录
ll                         ## 查看当前目录下的所有文件
tar -zxvf hmdp.tar.gz      ## 解压

不知道是不是我压缩的有问题,我的压缩文件将其父目录也压缩进去了
在这里插入图片描述

执行下面的命令:

mv hmdp /usr/local/nginx/html  ## 将前端项目移动到/usr/local/nginx/html下
rm -rf Users   ## 删除 Users 文件夹

3.3 修改 Nginx 的配置文件 nginx.conf

由于视频中 Nginx 和 Java 代码都是在同一个服务器上,所以 nginx.conf 中的服务器地址用的是 127.0.0.1,但是现在由于我将 Nginx 和 Java 代码分开来放了,就需要修改服务器地址了,将其修改成 Java 代码所部署的服务器地址,即 Mac 本身的 IP 地址。
在这里插入图片描述

3.4 启动 Nginx

cd /usr/local/nginx/sbin
./nginx

3.5 防火墙开启 8080 端口

firewall-cmd --permanent --add-port=8080/tcp  ## 添加端口
service firewalld restart   ## 重启防火墙

四、项目结构介绍

在这里插入图片描述
DTO(data transfer object):数据传输对象
是一种设计模式之间传输数据的软件应用系统,数据传输目标往往是数据访问对象从数据库中检索数据
数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具任何行为除了存储和检索的数据(访问和存取器)
简而言之,就是接口之间传递的数据封装
表里面有十几个字段:id,name,gender(M/F),age……
页面需要展示三个字段:name,gender(男/女),age
DTO由此产生,一是能提高数据传输的速度(减少了传输字段),二能隐藏后端表结构
具体可以查看这篇文章关于JAVA Bean实体类对象pojo,vo,po,dto,entity之间的区别,了解entity 和 DTO 之间的具体区别。

五、基础代码

基础代码不再过多介绍了

六、基于 session 实现短信登录

6.1 登录流程分析

在这里插入图片描述

6.2 实现发送验证码

在这里插入图片描述

UserController 类

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate IUserService userService;/*** 发送手机验证码*/@PostMapping("code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// 发送短信验证码并保存验证码return userService.sendCode(phone, session);}
}

UserService 接口

public interface IUserService extends IService<User> {Result sendCode(String phone, HttpSession session);
}

UserServiceImpl 实现类

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic Result sendCode(String phone, HttpSession session) {// 1、校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2、如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3、符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4、保存验证码到 sessionsession.setAttribute("code", code);// 5、发送验证码log.debug("发送短信验证码成功,验证码:{}", code);return Result.ok();}
}

6.3 实现短信验证码登录和注册功能

在这里插入图片描述

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1、校验手机号String phone = loginForm.getPhone();String code = loginForm.getCode();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 2、校验验证码String cacheCode = (String) session.getAttribute("code");// 3、不一致,报错if(cacheCode == null || !cacheCode.equals(code)){return Result.fail("验证码错误!");}// 4、一致,根据手机号去查询用户User user = query().eq("phone", phone).one();// 5、判断用户是否存在if(user == null){// 6、不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7、保存用户信息到 session 中session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();}/*** 根据手机号创建用户* */private User createUserWithPhone(String phone) {// 创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));// 保存用户save(user);return user;}
}

此处登录后会跳转到主页,而不是个人信息页面,这个是因为前端页面在登录后将跳转页面写成了 index.html,手动改一下即可。
在这里插入图片描述

6.4 登录校验拦截器

登录方法虽然已经创建完成,但是通过实际验证,此时依然无法登录,会直接跳转回登录页面。打开 info.html 页面,我们来分析下:
在这里插入图片描述
可以看到在 script 代码块中,当 Vue 实例化时,调用了一个 created() 函数,该函数又调用了 queryUser() 方法。
来看下 created() 函数的作用:
vue.js中created方法是一个生命周期钩子函数,一个vue实例被生成后会调用这个函数。一个vue实例被生成后还要绑定到某个html元素上,之后还要进行编译,然后再插入到document中。每一个阶段都会有一个钩子函数,方便开发者在不同阶段处理不同逻辑。
原文Vue进阶(三十六):created() 详解
也就是说在 Vue 实例化后,其实就直接调用了 queryUser() 方法,该方法调用了后台接口 /user/me,如果该接口没有获取到用户信息,则直接跳转至登录页面。该接口我们目前并没有进行实现,所以也就获取不到用户信息。现在来思考下如何实现这个接口。首先,这个接口没有任何的入参,也就是说我们没法根据用户的 id 或者手机号去数据库中获取,那我们要如何获取当前用户的信息?我们在登录时,其实已经向 session 中保存了用户信息,当然可以从 session 中直接获取。我们现在考虑另一种实现方式。
项目中部分模块的部分页面在用户未登录的状态是可以进行浏览的,但是还有部分模块的部分页面是必须在登录后才可以查看,比如个人信息页面。我们不可能对所有的模块都单独添加判断登录状态逻辑,那可不可以将登录状态的判断逻辑抽取出来形成一个公共方法,所有页面在访问时都需要先进性判断登录状态,符合条件的则放行,不符合条件的则跳转至登录页面。有一种方式可以实现,就是拦截器。我们通过拦截器拦截用户的所有请求,判断用户是否已经登录,如果已经完成登录,则将用户信息放到 ThreadLocal 中,保证每个请求可以获取到专属于它自己的用户信息,保证不同请求以及不同用户之间不会相互干扰。当用户已经处于登录状态,在我们访问 /user/me 接口时,自然也要进行判断,然后获取用户信息时,则可以直接从 ThreadLocal 中获取,而不是从 session 中获取。

知识点补充:
在这里插入图片描述
Session 是如何知道当前登录用户是哪一个用户的?
HTTP 是无状态的协议。所谓无状态,是指当一个浏览器客户程序与服务器之间多次进行基于 HTTP 请求/响应模式的通信时,HTTP 协议本身没有提供服务器连续跟踪特定浏览器端状态的规范。即 HTTP 协议本身是不会记录当前登录用户的信息的,所以当大量的用户去访问同一个页面时是无法通过 HTTP 协议去判断每个请求是由哪一个用户发出的。但是,我们又必须要区分出是哪一用户操作的。在 Web 开发领域,会话机制是用于跟踪客户状态的普遍解决方案。会话是指在一段时间内,单个客户与 Web 应用的一连串相关的交互过程。在一个会话中,客户可能会多次请求访问 Web 应用的同一个网页,也有可能请求访问同一个 Web 应用中的多个网页。
HTTP 会话为跟踪客户状态提供了统一的解决方案,SessionID 就是 HTTP 请求中用于跟踪客户状态的额外数据。当浏览器第一次请求访问应用中的任意一个支持会话的网页时,Servlet 容器试图寻找 HTTP 请求中表示 Session ID 的 Cookie,由于还不存在这样的 Cookie,因此就认为一个新的会话开始了,于是创建一个 HttpSession 对象,为它分配唯一的 Session ID,然后把 Session ID 作为 Cookie 添加到 HTTP 响应结果中。浏览器接收到 HTTP 响应结果后,会把其中表示 Session ID 的 Cookie 保存在客户端。当浏览器继续访问应用中的任意一个支持会话的网页时,在本次的 HTTP 请求中会包含表示 Session ID 的 Cookie。Servlet 容器视图寻找 HTTP 请求中表示 Session ID 的 Cookie,由于此时能获得这样的 Cookie,因此认为本次请求已经处于一个会话中,Servlet 容器不再创建新的 HttpSession 对象,而是从 Cookie 中获取 Session ID,然后根据 SessionID 找到内存中对应的 HttpSession 对象。

总结一下:
Session 是记录在服务器端的,用于跟踪用户状态的数据,而 Cookie 则是保存在客户端浏览器的用来保存用户信息的数据,属于 Session 的一种实现方式。Cookie 会记录每一个 Session 的 Session ID,该 Session ID 是唯一的,服务器可以根据该 Session ID 拿到对应的 Session。

代码实现

LoginInterceptor

package com.hmdp.utils;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1、获取 sessionHttpSession session = request.getSession();// 2、通过 session 获取用户信息、Object user = session.getAttribute("user");// 3、判断用户是否存在if(user == null){// 不存在报401response.setStatus(401);return false;}// 存在,将其保存到 ThreadLocal 中UserHolder.saveUser((User) user);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户,避免内存泄露// 视图渲染完成后执行 afterCompletion 方法,也就是说一次请求获取一次用户信息,用完立即释放UserHolder.removeUser();}
}

UserHolder

package com.hmdp.utils;import com.hmdp.entity.User;public class UserHolder {private static final ThreadLocal<User> tl = new ThreadLocal<>();public static void saveUser(User user){tl.set(user);}public static User getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}

MvcConfig

拦截器创建完成后并没有生效,还需要进行配置,MvcConfig 主要就是来让拦截器生效。

SpringMVC 中的拦截器有三个抽象方法:

  • preHandle:控制器方法执行之前执行 predHandle(),其 boolean 类型的返回值表示是否拦截或放行,返回 true 为放行,即调用控制器方法;返回 false 表示拦截,即不调用控制器方法。

  • postHandle:控制器方法执行之后执行 postHandle()

  • afterCompletion:处理完视图和模型数据,渲染视图完毕之后执行 afterCompletion()

原文:Spring MVC 之拦截器

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MvcConfig implements WebMvcConfigurer {// addInterceptors 添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加拦截器并排除不需要拦截的路径,即不用登录也可以访问的页面registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/user/login","/user/code","/shop-type/**","/upload/**");}
}

UserController

package com.hmdp.controller;import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.utils.UserHolder;@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/me")public Result me(){// 获取当前登录的用户并返回UserDTO user = UserHolder.getUser();return Result.ok(user);}
}

6.4 隐藏用户敏感信息

/user/me 接口获取到的用户信息,其实是一个完整的用户信息,当中包含有一些敏感信息,以及一些我们用不到的信息,这样一方面会暴露用户的隐私,产生风险,比如密码;另外一方面多余的信息会给系统增加压力。有没有什么好的办法解决这个问题呢?使用 UserDTO,将一些不需要的信息排除,只保留 User 类中我们需要用到的用户信息。

UserDTO:

package com.hmdp.dto;import lombok.Data;@Data
public class UserDTO {private Long id;private String nickName;private String icon;
}

来跟 User 实体类做个对比

package com.hmdp.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;import java.io.Serializable;
import java.time.LocalDateTime;@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 手机号码*/private String phone;/*** 密码,加密存储*/private String password;/*** 昵称,默认是随机字符*/private String nickName;/*** 用户头像*/private String icon = "";/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;}

可以看到 UserDTO 中去除了大量的用不到以及部分敏感的信息。

LoginInterceptor 中将 User 替换成 UserDTO

package com.hmdp.utils;import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1、获取 sessionHttpSession session = request.getSession();// 2、通过 session 获取用户信息、Object user = session.getAttribute("user");// 3、判断用户是否存在if(user == null){// 不存在报401response.setStatus(401);return false;}// 存在,将其保存到 ThreadLocal 中UserHolder.saveUser((UserDTO) user);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

将 UserHolder 中的 User 替换成 UserDTO

package com.hmdp.utils;import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user){tl.set(user);}public static UserDTO getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}

将 /user/me 接口中的 User 替换成 UserDTO

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/me")public Result me(){// 获取当前登录的用户并返回UserDTO user = UserHolder.getUser();return Result.ok(user);}
}

UserServiceImpl 中的 login 方法

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1、校验手机号String phone = loginForm.getPhone();String code = loginForm.getCode();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 2、校验验证码String cacheCode = (String) session.getAttribute("code");// 3、不一致,报错if(cacheCode == null || !cacheCode.equals(code)){return Result.fail("验证码错误!");}// 4、一致,根据手机号去查询用户User user = query().eq("phone", phone).one();// 5、判断用户是否存在if(user == null){// 6、不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7、保存用户信息到 session 中session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();}
}

6.5 集群的 Session 共享问题

多台 Tomcat 之间并不共享 Session 存储空间,当请求切换到不同的 Tomcat 服务时就会产生数据丢失问题。
在这里插入图片描述
当我们搭建负载均衡集群时,请求会在每个 Tomcat 之间进行轮询,假设请求第一次被负载均衡到了 Tomcat1,相关的一些用户信息就会被保存到 Tomcat1 中,那么当用户第二次请求时,如果被负载均衡到了 Tomcat2 上,此时 Tomcat2 上并没有保存用户信息,这时就会产生问题。其实,Tomcat 之间可以实现 Session 共享,只是该种方案会有以下两个问题:
1、Tomcat 之间实现 Session 共享会进行数据拷贝,多份相同的数据浪费内存空间
2、数据拷贝需要一定的时间,会产生延时,如果正好在拷贝期间进行了请求,那么同样也会访问不到,产生问题。

Session 替代方案应该满足以下三个条件:

  • 数据共享,无需进行数据拷贝
  • 内存存储,因为 Session 是基于内存,效率高,那么替代方案也应当满足该条件
  • key、value 结构,Session中的数据保存为 key、value 格式,那么替代方案也应当满足

Redis 可以满足上述要求。

6.6 基于 Redis 实现共享 Session 登录

6.6.1 分析

在这里插入图片描述
由 Session 改为 Redis 存储验证码,整体流程上没有太多变化。但是,由于 Redis 是以键值对存储的,其值存储有多种形式,那么当前情况下,哪种形式的值存储比较合适呢?由于我们需要存储的是一个 6 位数字类型的值,而 String 类型的数据结构就完全可以满足要求了。但是,键值该如何存储呢?原先 Session 存储我们以 “user" 作为 key,由于 Session 的特性(每个浏览器在发出请求时都有独立的 Session),虽然不同的用户有着相同的 key,但是不同 Session 之间相互不干扰,这就使得服务器端可以根据不同的 Session 识别出是哪个用户做出的操作。而 Redis 中的数据是共享的,如果再以 ”user“ 作为 key,不同的用户在向 Redis 中存储 value 时会产生覆盖问题,我们需要保证每个不同的手机号在做验证时保存的 key 都是不一样的,那我们就可以直接以手机号作为 key 来存储验证码,这样就可以保证每一个不同的手机号都有自己唯一的 key。这样还有一个好处就是,我们在做短信验证码登录时,手机号方便客户端携带,服务器端收到请求后,可以直接以手机号为 key 来读取验证码。
根据上面的流程图,我们在登录验证时,如果从数据库中查询到用户,还需要将用户的信息保存到 Redis 中,此时该以何种 Redis 数据结构来存储用户数据呢?来看下 String 类型和 Hash 类型的区别:
在这里插入图片描述
可以看出,Hash 结构胜出。
那么又该选用什么形式的 key 来存储用户数据呢?
在这我们选择随机的 token 作为 key 来存储用户数据。在短信验证码登录时,我们还需要将这个随机 token 返回给客户端,这是因为后期我们在访问各个页面时都是需要校验登录状态,来判断哪些页面用户可以在未登录状态下访问,哪些页面需要登录后才能访问。
在这里插入图片描述
来看下前端是如何存储 token 的。
当我们访问接口 /user/login 时,如果访问成功,会将 token 返回给前端登录页面,而前端则会将该 token 保存到 session 中(通过 sessionStorage.setItem 方法)。
在这里插入图片描述
再来看下 common.js
可以看到,common.js 中将 token 数据保存到了请求头中,该请求头的名字叫做”authorization“,这样在后续所有的 Ajax 请求中,都会在请求头中携带该 token。
在这里插入图片描述
而在此处为什么没有使用手机号作为 token 呢?这是因为 token 需要保存在客户端,如果以手机号作为 token,会有泄露用户隐私的风险。

6.6.2 代码实现

package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpSession;import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {// 1、校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2、如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3、符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4、保存验证码到 Redis 中stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5、发送验证码log.debug("发送短信验证码成功,验证码:{}", code);return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1、校验手机号String phone = loginForm.getPhone();String code = loginForm.getCode();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 2、从 Redis 中获取验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);// 3、不一致,报错if(cacheCode == null || !cacheCode.equals(code)){return Result.fail("验证码错误!");}// 4、一致,根据手机号去查询用户User user = query().eq("phone", phone).one();// 5、判断用户是否存在if(user == null){// 6、不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7、将用户信息保存到 Redis 中// 7.1 生成 token,此处使用 UUID 作为 tokenString tokenKey = UUID.randomUUID().toString(true);// 7.2 将 User 对象转为 HashMap 存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);// 7.3 将用户信息存储到 Redis 中stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + tokenKey, userMap);// 7.4 设置过期时间stringRedisTemplate.expire(LOGIN_USER_KEY + tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 将 token 返回给客户端return Result.ok(tokenKey);}/*** 根据手机号创建用户* */private User createUserWithPhone(String phone) {// 创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));// 保存用户save(user);return user;}
}

代码实现分析:
1、sendCode 方法,将 session 存储验证码改为 Redis 存储,同时设置过期时间
2、login 方法中,将 session 存储用户信息,修改为 Redis 存储,value 值采用 Hash 类型,同时设置过期时间,模拟 session 过期时间。但是 session 过期是在用户未作任何操作的情况下,而 Redis 则是从用户登录开始计时,到指定时间后自动过期。我们应当保证只要用户在不断访问,就不断更新 Redis 中的 token 过期时间。那我们如何知道用户什么时候访问,有没有访问呢?我们所有的请求都要经过拦截器进行校验,只要同过了拦截器的校验就说明用户是已经登录的且在活跃的状态。那么我们就可以在拦截器中对 token 的过期时间进行刷新操作。只有什么都不操作的情况下,才不会走拦截器的校验,也就不会刷新 token 的过期时间。

修改拦截器

这里要注意一点就是,LoginInterceptor 是我们自定义的一个类,并非 Spring 进行管理的类,所以在使用 StringRedisTemplate 的时候,无法使用 @Autowired 或者 @Resource 进行注入。但是,MvcConfig 是由 Spring 进行管理的,可以由 Spring 注入 StringRedisTemplate 的实例。

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1、从请求头中获取 tokenString token = request.getHeader("authorization");// 2、判断token是否为空if (StrUtil.isBlank(token)) {// 不存在报401response.setStatus(401);return false;}// 3、根据 token 从 redis 中获取用户信息String tokenKey = RedisConstants.LOGIN_USER_KEY + token;// 使用 entries 方法获取所有的 field-valueMap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);// 4、判断 userMap 是否为空if (userMap.isEmpty()) {// 不存在报401response.setStatus(401);return false;}// 4、将 userMap 转换为 UserDTOUserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 5、将 user 保存到 ThreadLocal 中UserHolder.saveUser(user);// 6、刷新 token 过期时间stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

修改 MvcConfig 配置类

在 MvcConfig 中注入 StringRedisTemplate,同时在添加拦截器时,将 StringRedisTemplate 的实例通过 LoginInterceptor 的构造器传入。

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/shop/**","/voucher/**","/user/login","/user/code","/shop-type/**","/upload/**");}
}

6.6.3 解决登录时类型转换异常bug

重启项目后点击登录时,发现后台报了如下的错误。
在这里插入图片描述
这是因为 userMap 中的 id 为 Long 类型,但是 Redis 中存储的都是 String 类型。
在这里插入图片描述
login 方法改进,将 UserDTO 实例转换为 HashMap 时,将每一个属性转换为 String 类型。

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1、校验手机号String phone = loginForm.getPhone();String code = loginForm.getCode();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 2、从 Redis 中获取验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);// 3、不一致,报错if(cacheCode == null || !cacheCode.equals(code)){return Result.fail("验证码错误!");}// 4、一致,根据手机号去查询用户User user = query().eq("phone", phone).one();// 5、判断用户是否存在if(user == null){// 6、不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7、将用户信息保存到 Redis 中// 7.1 生成 token,此处使用 UUID 作为 tokenString tokenKey = UUID.randomUUID().toString(true);// 7.2 将 User 对象转为 HashMap 存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3 将用户信息存储到 Redis 中stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + tokenKey, userMap);// 7.4 设置过期时间stringRedisTemplate.expire(LOGIN_USER_KEY + tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 将 token 返回给客户端return Result.ok(tokenKey);
}

总结

Redis 代替 session 需要考虑的问题:

  • 选择合适的数据结构
  • 选择合适的 key
  • 选择合适的存储粒度

6.7 登录拦截器的优化

6.7.1 分析

当前拦截器:
在这里插入图片描述
登录功能是基于拦截器做的校验功能,但是当前拦截器拦截的并不是所有的路径,而是拦截的需要登录的路径,如果用户登录后,一直访问的是首页这种不需要拦截的路径,那么拦截器就会一直不执行,token 的过期时间就不会刷新,那么当 token 过期后,用户访问例如像个人主页时就会出现问题,很不友好。那如何解决?可以在当前拦截器的基础再添加一个拦截器,让新的拦截器拦截一切路径,在该拦截内做 token 的刷新动作。

优化后的拦截器:
在这里插入图片描述

6.7.2 创建 RefreshTokenInterceptor 拦截器

该拦截器用于拦截所有请求,并刷新 token 过期时间

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1、从请求头中获取 tokenString token = request.getHeader("authorization");// 2、判断token是否为空if (StrUtil.isBlank(token)) {return true;}// 3、根据 token 从 redis 中获取用户信息String tokenKey = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);// 4、判断 userMap 是否为空if (userMap.isEmpty()) {return true;}// 4、将 userMap 转换为 UserDTOUserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 5、将 user 保存到 ThreadLocal 中UserHolder.saveUser(user);// 6、刷新 token 过期时间stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

6.7.2 修改 LoginInterceptor 拦截器

LoginInterceptor 则只需要判断当前访问用户是否已经登录,已经登录则放行,未登录则拦截

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断当前用户是否登录if (UserHolder.getUser() == null) {// 未登录,设置状态码response.setStatus(401);// 拦截return false;}return true;}}

MvcConfig 设置拦截规则

拦截器执行顺序应当是先执行 RefreshTokenInterceptor,而后再执行 LoginInterceptor,通过 orde 方法设置拦截器执行顺序,值越小,则执行顺序越优先。

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/user/login","/user/code","/shop-type/**","/upload/**").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/21169.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023最新匿名短信【时光送信】H5源码V4.0版+后端管理

如果把这个做成副业那将大大增加你每天的收益像这种看似简单&#xff0c;非常冷漠小众的项目&#xff0c;往往可以带给你超高收益&#xff0c;而且每条短信成本1毛都不到 里面就包含这三个功能&#xff0c;看似不起眼对学生或者正在谈恋爱的人很有用 发出去的短信就是这种&…

Smartbi观点 | ChatGPT还处于初级阶段?然而AI早已打入BI内部

最近&#xff0c;当我们还沉浸在电影《流浪地球2》MOSS所带来的震感时&#xff0c;ChatGPT又火爆社交媒体&#xff0c;成为全球“新顶流”。 官方数据显示&#xff0c;今年1月&#xff0c;平均每天约有1300万独立访客使用 ChatGPT&#xff0c;累计用户超1亿&#xff0c;创下了…

录用2360篇、接收率25.78%,CVPR 2023接收结果公布

关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;ComputerVisionGzq 学习群&#xff5c;扫码在主页获取加入方式 计算机视觉研究院专栏 作者&#xff1a;Edison_G 你中了吗&#xff1f; 转自《机器之心》 接收率出来了&#xff01;在短短几个小时内&#xff0c;各…

ChatGPT的阴谋~第1125位投票者的时间轴

2023年3月29日&#xff0c;媒体都在转发马斯克的公开信&#xff0c;大意是暂停一切大型AI研发至少6个月&#xff0c;并在此期间制定相应方案管控AI。虽然公开信只有595个单词&#xff0c;却被世界众多顶级教授&#xff0c;科技界联合创始人一致认同。 10日后&#xff0c;OpenA…

文心一言 VS chatgpt (17)-- 算法导论4.1 3~4题

三、在你的计算机上实现最大子数组问题的暴力算法和递归算法。请指出多大的问题规模n0是性能交叉点一一从此之后递归算法将击败暴力算法?然后&#xff0c;修改递归算法的基本情况一一当问题规模小于 n0 时采用暴力算法。修改后&#xff0c;性能交叉点会改变吗? 文心一言&…

文心一言 VS chatgpt (2)-- 算法导论1.2

一.给出在应用层需要算法内容的应用的一个例子&#xff0c;并讨论涉及的算法的功能。 文心一言&#xff1a; 在应用层需要算法内容的应用的一个例子是无人机自主飞行控制。无人机自主飞行控制需要算法来确定无人机的位置、速度和方向&#xff0c;以便在复杂的环境中实现精确的…

chatgpt赋能Python-pythonzero

Pythonzero&#xff1a;让您以零门槛学习Python的最佳平台 什么是Pythonzero Pythonzero是一种基于Python编程语言的在线教育平台&#xff0c;旨在向初学者以及那些想要进一步提高的人提供学习Python的指导。通过Pythonzero&#xff0c;您将能够快速上手编写Python代码&#…

某程序员哀叹:发现技术人的通病是不擅长汇报,执行力100分,汇报只能讲60分!怎样才能提高汇报能力?...

会做不会说是大多数互联网技术型人才的通病&#xff0c;一位程序员说&#xff1a;因为工作偏逻辑性且结果导向&#xff0c;导致汇报能力超级差&#xff0c;比如想法和执行力100分&#xff0c;写到报告里只有80分&#xff0c;汇报时只能讲出60分&#xff0c;这种问题在自己和周围…

linux 添加环境变量 永久,Linux(CentOS) 下设置永久环境变量(export PATH)

在安装完 LAMP 环境后&#xff0c;如果我们想使用 php 或 mysql 的命令&#xff0c;那么我们必须得到其安装目录下执行&#xff0c;比如&#xff1a; [root www.linuxidc.com ~]# /usr/local/php/bin/php index.php //使用 php 命令运行 index.php 文件 这样做&#xff0c;比较…

用python做副业,月赚1W+,别被死工资拖累!

被压垮的打工人&#xff0c;你还好吗&#xff1f;房贷车贷&#xff0c;上老下小&#xff0c;日常开销&#xff0c;但你的收入有多少&#xff1f;&#xff1f;&#xff1f;所以你不敢生病&#xff0c;甚至不敢回家&#xff01;就为了每个月那么点死工资&#xff0c;还得天天加班…

【Python赚钱思路】如何利用Python业余时间月赚1k~6k不等?

关于Python&#xff0c;如何利用Python技术变现 & 兼职接单也是大家比较感兴趣的&#xff1b; 这里总结了一些用Python赚外快的方式&#xff0c;大家伙可以自己去尝试一下。 Python兼职分为以下三种&#xff1a; 商家提供接口爬取数据&#xff08;当然不做违法的爬取&…

chatgpt赋能python:Python商场打折:省钱又省力

Python商场打折&#xff1a;省钱又省力 在当前经济形势下&#xff0c;消费者对价格的敏感性越来越高。越来越多的商家为了吸引消费者&#xff0c;不断推出各种打折方法。而我们作为消费者&#xff0c;当然是希望能够买到价格优惠的商品。这就需要我们不断地寻找各种商场打折信…

靠谱!我找到了用AutoGPT+python爬虫搞钱的新路子!

近几个月真是太魔幻了&#xff0c;优秀的AI接连问世&#xff0c;原本ChatGPT3.5的表现就足够震撼了&#xff0c;现在又来了一个更重磅的东西——AutoGPT&#xff01; 它是一个由GPT-4驱动&#xff0c;能自主完成各项任务&#xff0c;几乎不需要人类插手的新AI产品。有了AutoGP…

chatgpt赋能python:如何用Python获取数据

如何用Python获取数据 Python 是目前使用最广泛的编程语言之一&#xff0c;不仅适用于各类科学计算、统计分析、图形处理等领域&#xff0c;还广泛应用于Web开发中。本篇文章将介绍如何使用Python获取数据&#xff0c;通过Python获取所需的数据可以大幅提高SEO工作的效率。 方…

chatgpt赋能python:Python自动爬取优惠券,助你省钱无忧

Python自动爬取优惠券&#xff0c;助你省钱无忧 在这个物价上涨的时代&#xff0c;大家都希望能够省下一些钱&#xff0c;所以优惠券成为了很多人的首选。而手动在各大电商平台找优惠券会比较麻烦&#xff0c;因此&#xff0c;使用Python进行自动爬取优惠券&#xff0c;就变得…

从零开始教你学爬虫!python爬虫的基本流程!

网络爬虫是什么&#xff1f; 网络爬虫就是&#xff1a;请求网站并提取数据的自动化程序 网络爬虫能做什么&#xff1f; 网络爬虫被广泛用于互联网搜索引擎或其他类似网站&#xff0c;可以自动采集所有其能够访问到的页面内容&#xff0c;以获取或更新这些网站的内容和检索方…

SpringBoot仿GPT数据流传输

目录 Java数据流传输响应前提Springboot文字流响应Web端接收流数据并显示 SpingBoot集成ChatGPT使用流响应结果 Java数据流传输响应 前提 在折腾ChatGpt集成在SpringBoot项目时&#xff0c;发现了ChatGpt api返回数据时有两种返回方式&#xff0c;一种是使用流传输&#xff0…

【SpringBoot】SpringBoot整合Nginx的全部流程

SpringBoot整合Nginx的全部流程 对Nginx还不了解的同学可以先看这篇文章Nginx 相关介绍(Nginx是什么?能干嘛?) 今天的目标是将SpringBoot项目由默认部署方式(jar)替换成war形式&#xff0c;部署在同一台电脑上的两个不同端口的tomcat上&#xff0c;利用Nginx做反向代理&…

Excel数据动态看板制作:数据处理、数据分析、看板制作、插入切片器、图表类型

Excel数据动态看板制作-以教师薪酬统计为例 一、数据处理二、数据分析三、看板制作四、插入切片器五、图表类型 原始数据如图所示&#xff1a; 一、数据处理 1、工龄计算&#xff1a;DATEDIF(G3,TODAY(),“Y”) 2、工龄工资计算&#xff1a;IF(H350>500,500,H350) 3、…

网页在线编辑表格|仿Excel|特定表头后超级爽

最近公司开发的EMIS系统有个模块需要按excel格式写&#xff0c;原先有个estartable插件&#xff0c;我们经理写的&#xff0c;在原来的模块上面很好用&#xff0c;由于我水平有限&#xff0c;我在短期内不能清晰的修改或扩展它&#xff0c;最近掌握了angularJS&#xff0c;突发…