项目背景:
该项目背景就是黑马的黑马点评项目。
一:基于Session实现验证码登录流程
基本的登录流程我们做了很多了。这个是短信登录流程
其实和普通的登录流程就多了一个生成验证码,并将验证码保存在session中,并且呢,返回给前端,前端登录的时候需要多提交一个表单项而言
整体流程:
整体的流程就是生成了一个验证码并且保存在session中
当用户登录时,我们就在session中取出对应的验证码然后和用户传的验证码是否相同来比对
如果用户不存在,就注册之后,将用户存储到当前的线程中
如果用户存在,直接存储到当前的线程中
这个是前端拦截器的实现
关于这个cookie和session的会话技术,中间具体的过程就是:
当登录成功之后,在tomcat服务器会生成具有唯一ID的session,前端会有一个Cookie保存在浏览器中,每次前端发送请求,就会带上这个Cookie,根据这个Cookie会匹配一个Session,我们就可以通过这个Session是否存在来知道用户是否登录
具体的在:SpringMVC(包括Servlet,会话技术)理解-CSDN博客
这里需要有一个重要的思维:就是Tomcat也是有内存的,
在一开始的时候,黑马的课是直接将整个User实体类存储到当前线程中,但User实体类属性很多,会占用更多的内存,所以后面就存储UserDTO这个类,相对实体类属性较少,会节省空间,所以我们需要对Tomcat的线程和内存的量都需要有概念,可能以后项目优化就可以从这个点下手。
Redis代替session进行短信验证登录流程
首先为什么要用这个Redis代替传统的Session进行存储呢?
这个是关于分布式的知识:
Redis-CSDN博客这一篇有
先来看个图也行
具体是下面的五个步骤:
1:先将生成的验证码放到redis中
@Override
public Result sendCode(String phone, HttpSession session) {
//1: 对传递的电话进行合法校验
if (RegexUtils.isPhoneInvalid(phone)) {
//2:不合法直接返回
return Result.fail("手机号不合法");
}
//3:合法,生成验证码
final String code = RandomUtil.randomNumbers(6);
//4:将验证码保存到Redis中,且key为当前用户手机号
redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code);
//5:模拟发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
//6:返回
return Result.ok();
}
整体的代码逻辑:
先用正则表达式来校验电话是否合法
再用Hutool的RandomUtil生成6位随机数字
最后将具有业务意义的Redis常量+用户的电话做为Key存储到redis中
最后只是模拟发送了验证码
2:login登录接口去redis中查找验证码
3:login登录接口中将登录成功之后的用户信息存储到redis中
这里我们需要先明确的一点是:
我们用Redis中的那种数据结构来存储用户信息
我们常用的String,如果用来存储的话,肯定可以不过不便于扩展
最好的数据结构是Hash
这两步在一个方法中
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
//1:校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号不合法");
}
//校验验证码
// 从redis中取出验证码
String cacheCode = (String) redisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
if (StringUtils.isBlank(code)
|| StringUtils.isBlank(cacheCode)
|| !code.equals(cacheCode)) {
return Result.fail("验证码不能为空或验证码错误");
}
//2:根据手机号查询用户
User user = query().eq("phone", phone).one();
//3:如果这个用户不存在,就创建新用户
if (user == null) {
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
boolean save = this.save(user);
if (!save) {
return Result.fail("创建用户失败");
}
}
//4:保存用户到Redis 这里要有一个内存的概念,就是你将整个User实体全都存进去,占用的内存就比UserDTO大很多
//4.1:生成token
String token = UUID.randomUUID().toString();
//4.2:将User对象转为UserDTO
final UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//4.3:将UserDTO转换成map对象
final Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
redisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
//4.4:设置token的过期时间
redisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
一般我们会将Redis的Key中的字符串抽象成一个常量
package com.hmdp.utils;
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 36000L;
}
整体的代码逻辑:
先用正则表达式来校验电话是否合法
用相同的Key从redis中取出验证码进行校验
根据手机号查询用户,根据是否存在然后进行注册或者登录
生成一个token,这个就是redis中的key,并且将这个token返回给前端,前端请求其它接口的时候,在请求头中带上token,后端根据这个token在redis中查找
这里生成token之后,还需要将用户User(从数据库中查出来的)转成UserDTO,在转成Map
这里可以用一个Hutool的BeanUtil.beanToMap,并且呢
这里有一个问题就是,UserDTO中有一个字段是Long类型的,我们的userMap都是<String,Object>,所以我们可以用这个工具类提供的第三个参数:
CopyOptions.create()
.setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()))
来自定义我们自己的转换规则
最后我们再设置一下过期时间,过期时间的作用肯定不用多说,非常重要。
但是我们现在这样写还是有问题:就是我们已经设置了这个redis中的session过期时间,但是,我们会发现,我们就只在这个登录接口中设置了,比如用户调用了其它的接口,这说明用户还是在我们这个应用中,不过到时间了,用户的redis中的session自动过期,直接下线,这样用户体验不好,所以,我们下一个解决策略就是在拦截器中刷新这个过期时间。
4:在拦截器中重新刷新redis的过期时间,并且将用户信息保存到ThreadLocal中
但这里我们可以同时把第五个步骤抛出来
就是:我们光在拦截器这里刷新就够了嘛?比如有些接口在这个LoginInterceptor中没有被拦截到),我们在这个LoginInterceptor之前再加一个接口,专门做redis的刷新,在下一个接口再进行拦截,有点像微服务网关后面再加一个这个MVC拦截器的流程一样
5:解决一些未被第一个拦截器拦截到的路径的接口redis过期的问题
package com.hmdp.utils;
public class RefreshRedisIntercepor implements HandlerInterceptor {
private RedisTemplate redisTemplate;
public RefreshRedisIntercepor(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.3:从前端请求中取出token
final String token = request.getHeader("authorization");
//1.4:判断token是否为空
if(StringUtils.isBlank(token)){
return true;
}
//1.6:从redis中取出userMap
final Map userMap = redisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
//1.7 :判断userMap是否为空
if (userMap.isEmpty()) {
return true;
}
// 手动转换 id 字段
if (userMap.containsKey("id")) {
Object idValue = userMap.get("id");
if (idValue instanceof String) {
userMap.put("id", Long.valueOf((String) idValue)); // 转换为 Long
}
}
final UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//2:保存用户的信息到ThreadLocal
UserHolder.saveUser((UserDTO) userDTO);
//3:刷新Redis的过期时间
redisTemplate.expire(RedisConstants.LOGIN_USER_KEY + userDTO.getId(), RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//3:在请求结束后,移除ThreadLocal中的用户信息
UserHolder.removeUser();
}
}
这个是Redis刷新拦截器
上面的注释就是代码流程了
这里有一个小小的思维点,就是这个拦截器,叫拦截器,但是它不拦截,就是如果你的参数不合法,我不拦截,我直接给你通过,到下一个点去去拦截
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginIntercepor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1:判断ThreadLocal是否有userDTO
final UserDTO user = UserHolder.getUser();
if(user == null){
response.setStatus(401);
return false;
}
return true;
}
}
这个是第二个拦截器,
这个拦截器的逻辑很简单,判断当前线程中是否有这个userDTO,如果没有,就拦截,有就放行
这里有个小技巧,
就是我们这个RefreshRedisIntercepor不是bean对象,所以不能依赖注入RedisTemplate。
我们可以在MvcConfig中进行配置:
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private RedisTemplate redisTemplate;
//配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercepor())
.excludePathPatterns("/user/code", "/user/login", "/shop/**", "/shop-type/**", "/upload/**", "/blog/hot", "/blog/search").order(1);
registry.addInterceptor(new RefreshRedisIntercepor(redisTemplate)).order(0);
//order :调整优先级
}
}
在RefreshRedisIntercepor留一个构造方法
在这个MvcConfig进行依赖注入
并且这里可以通过.order(0)这个方法进行优先级的排行