整体的工作流程
> 这里我直接使用redis了,而不是使用session。
1. 发送短信功能,会把验证码保存到redis上
2. 点击登录功能,校验你的手机号和验证码。将你当前登录账号的信息保存到redis上。
3. 校验登录状态,如果已经登录,判断redis上是否有该用户的信息。
发送短信功能
这里没有开启短信服务,所以使用简单的将验证码输入到控制台。
工作流程:
前端:
1. 点击【发送验证码】的时候,会先校验你的账号,如果你的账号不合理,是发送不了请求的。
后端:
- 接收到前端发送过来的请求,携带手机号码过来
- 后端再次校验你的手机号码是否合理
- 如果不合理,就直接返回错误信息即可。
- 如果合理,生成验证码
- 将验证码保存到redis上。注意这里的redis的 key 和 value的选择是很重要的,这个key我们是选择 【功能模块+手机号】,而value是选择String类型-->保存验证码
- 发送验证码
- 返回发送成功,这里不需要什么特别的返回值。
代码实现
请求的url地址:http://localhost:8080/api/user/code?phone=13434123456
请求的方式:post
请求参数是:phone
返回值:不需要
>请求Url上的:这个api表示是向tomcat发送请求的前缀,会被过滤掉。
- 手机的校验
- 验证码的生成
都是使用hutool包来做的
测试效果
已经能够发送成功了,但是还没办法完成登录。
点击登录的功能
工作流程
前端:
点击登录之后,将手机号码和验证码,携带过来发给送给后端
后端:
- 接收到前端发送过来的请求,携带手机号码,和验证码过来,和密码发送过来。所以我们需要一个entity来接收这个数据。我们定义一个LoginFormDto类。将前端发送过来的数据,保存到loginFormDto对象上
- 从loginFormDto对象,取出 phone这个值。然后再去校验这个phone是否合理
- 如果phone合理,通过phone从redis中找到对应的验证码
- 将loginFormDto上的验证码,和从redis上取出来的验证码作比较
- 如果验证码码不一样直接会犯错误。
- 如果验证码一样,判断当前手机号 是否在user表中存在,如果不存在,就创建user对象。
- 如果存在,直接存储到redis中。
- 然后返回的东西是什么,保存在redis上的key
代码实现
>
测试
登录成功的校验功能
点击某些页面,你是不能访问的,你必须先登录之后才能访问
> 如果我们已经完成了:【短信发送】、【点击登录】、着两个功能的逻辑,不代表我们已经能够登录了,还差点东西。就是登录校验,你登录成功之后得有一个登录凭证。这个登录凭证主要是给后端的。前端你已经将token返回给前端了,浏览器知道你有这个东西了。但是有哪些网页你访问不了,你访问了就会自动跳转,虽然说自动跳转是前端完成的,前端的跳转时跟你后端返回的数据来进行跳转的,如果你返回的数据是前端需要的,那它还跳转干嘛?
解下来就使用拦截器,来完成这个登录凭证
- 使用拦截器来完成这个功能。为什么呢?因为每一个网页,我们都需要去登录校验,判断你是否已经登录。所以在拦截器的时候,如果没有登录,我们就可以拦截下来。
执行流程
这里我们这里使用的redis,把session换成redis即可。
- 因为我们是拦截器对吧,首先网页请求过来一定会访问我们这个拦截器。
- 如果我们登录过,我们会把 token返回给浏览器,然后存储在请求上对吧。这个为什么会放在请求头上面,是前端做的,我们后端负责把token返回给前端。然后前端存储在请求里面,它的头部信息是:authorization 这是前端设计好的。
- 我们拦截器的第一步是:先判断是否已经登陆过了。
- 如果没有登录过,拦截下来。但是不能全部都取拦截,比如有些页面我们必须放行,比如登录页面,如果这个都拦住,我们就没办法登录了。
- 如果已经登录过了,直接放行。
代码实现
1. 后端必须有一个自定义拦截器类,去实现HandlerIntercetor接口。重写两个方法,一直是请求前拦截,一个请求结束后处理。 就是线程开启处理的逻辑,线程结束后处理的逻辑
ublic class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//请求开始前,执行的逻辑,这个就是拦截器。
//1. 我们先从请求头里面,取出token,token就是存储在redis上的健,该值就是登录的用户
String token = request.getHeader("authorization");//取出token
if(StrUtil.isAllBlank(token)){//如果取出来的token是不存在的,就说明没有登录过,直接拦截
return false;
}
//2.从redis上取出数据,从redis上取数据,就必须使用StringRedisTemplate。所以注入进来,但这里能使用@Authorized注入码?不行
// 为什么因为我们这个配置器类,并没有交给spring管理。所以如果要注入,就只能使用构造器注入
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
//3.判断这个entries是否为null
if(entries.isEmpty()){
//如果是null,直接返回错误信息就可以了
response.setStatus(401);//401状态码表示未登录的意思
//拦截器怎么返回 异常信息???没办法直接拦截就可以了
return false;
}
//如果说redis上有值,某些页面也需要使用到这个用户信息,我们把用户信息存储在哪里?
//如果我们的执行是一条龙,就是同一条线程的话,我们只需要把用户信息存储到ThreadLocal上
// 难点是什么:取出来的是map数据,怎么将其转换为UserDto对象?使用工具类,将map转为Bean
UserDTO userDTO = new UserDTO();
BeanUtil.fillBeanWithMap(entries,userDTO,false);//我记得前面,我们存储到redis上的时候,里面的value全是String,我这个Dto的Id是long可以直接接收的。
//接下来将UserDto保存到ThreadLocal上
UserHolder.saveUser(userDTO);
//保存到ThreadLocal上之后,我们去刷新一下在redis保存的时间。否则30分钟一到,我们这个redis就会过去。只要有访问到需要验证的页面,我们就会刷新token
stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);
//放行
return true;
}
//就是本次这个页面结束之后,执行的逻辑
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//当前线程结束,我们就把这个ThreadLocal里面的User清理掉
UserHolder.removeUser();
}
}
2. 定义好拦截器类之后,我们定义好一个mvc配置类,去实现WebMvcConfig接口,加载这个自定义拦截器。
2.1
> 当前这个类,添加了Configuration注解,所以当前这个配置类是交给spring管理的,这里我们需要添加拦截器对象。而我们的拦截器是没办法通过@Resoure或者@Autowire注入StringRedisTemplate。所以我们在MVC配置类,添加这个拦截器对象的时候,将StringRedisTemplate通过构造器注入的方式将StringRedisTemplate注入进去。
总之:就是我们在mvc配置类上,要通过@Resource注解,将redisTempalte注入进来,然后将其传给拦截器。
最后实现的效果,就是点击某些需要登录的页面之后,就会自动跳转到登录页面
登录校验功能优化
分析
1. 如果我们只使用一个拦截器去做,登录校验,是可以的,但有一个弊端。
就是当我们访问:需要登录校验的网页,我们才会去刷新token,当我们一直处于不需要登陆的校验的页面,30分钟后,我们的token就会消失,相当于我们自动退出了。即使ThreadLocal保存了用户信息,当我们遇到拦截器的时候,直接就先判断redis上的token,压根就进不去你的ThreadLocal就会被直接拦截。
所以了为了解决这个,一直处于不需要登录校验的页面,会自动退出的问题,我们就需要多一个拦截器。
思路分析
新拦截器需要做的事
- 获取请求头上的token
- 如果token为null,直接放行。这样ThreadLocal就没有数据了
- 如果token不为null,就从redis中找数据,因为我们前面存储userDto对象的时候,key是【功能模块+token】,value是hash类型。所以取出来的时候,可以各种取法,但是我们选择以键值对的形式取出来。就是取出来的是一个map集合
- 判断这个map集合是否为null,如果为null直接放行(这样ThreadLocal上就没有数据)
- 如果不为null,就重写写入到Redis上,会发生覆盖,然后重新设置存货时间。
旧拦截器:
- 只需要判断ThreadLocal是否null即可
- 如果为null就拦截,如果被拦截之后,前端页面就会自动跳转到登录页面
- 如果不为null,直接放行即可
代码实现
1. 新拦截器
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.先获取token数据,取出来的token是空串,直接放行,说明登录,让第二个拦截器去处理。
String token = request.getHeader("authorization");//取出token
if(StrUtil.isBlank(token)) {
return true;
}
//token不是空串,就去redis上取出对应的Value
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
if(entries.isEmpty()){
//如果取出来的是null,直接放行。这样ThreadLocal上就不会有数据,第二个拦截器是否拦截器本次请求,是根据ThreadLocal是否有数据来决定的
return true;
}
//这里让其创建UserDto,并保存到reids上,需要吗?肯定啊。否则第二个拦截器是根据你的ThreadLocal是否有值来决定,你是否拦截器的
// 如果你此时不将redis 根据token取出来的对象存放在threadLocal上,就会一直被拦截。
UserDTO userDTO = new UserDTO();
BeanUtil.fillBeanWithMap(entries,userDTO,false);
log.info("userDtoNickName:"+userDTO.getNickName());
UserHolder.saveUser(userDTO);
stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);
return true;
}
}
2. 旧拦截器
public class LoginInterceptor implements HandlerInterceptor {
// private StringRedisTemplate stringRedisTemplate;
// public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
// this.stringRedisTemplate = stringRedisTemplate;
// }
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//第二个拦截器,我们还需要从Redis取出值吗。没有必要了。所以把StringRedisTemplate删掉,或者注释掉
//我们当前这个拦截器,判断你ThreadLocal上的值是否为null,来决定是否拦截的,如果拦截了,前端会帮我们自动跳转到登录页面
UserDTO user = UserHolder.getUser();
if(user==null){
response.setStatus(401);
return false;
}
return true;
}
}
3. mvc配置类
@Configuration
public class MVCConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
//LoginInterceptor 拦截器,有那些是不能拦的?发送验证码(发出的请求)不能栏。点击登录之后的请求不能拦。还有一些页面你也不用去栏。
//可以给别人参观。
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/voucher/**"
).order(1);
//TODO 设置优先级,因为我们必须让这个自动刷新token的放在前面,这样我们访问不需要校验的页面,也会自动刷新token
// 并且新拦截器是拦截所有的,并且都会放行。
// 至于会不会拦截,留给第二个拦截器去判断。如果token存在,并且对应value里面有数据,就会保存到ThreadLocal。
// 第二个拦截器判断ThreadLocal里是否有数据,如果没有就会拦截
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}