一、分析问题
上节我们已经实现了基于短信验证码做登录的功能,不过可惜的是在页面上它依然认为我们没有做登录,其实就是因为登录校验的功能我们还没有做,这节我们就来做一个登录验证。
事实上登录验证就是一个这样的一个请求 /user/me
,它就是用来查询当前登录的用户信息,如果你能给它返回,那这个登录校验就算成功了。流程之前我们也分析过,用户请求都会带上cookie,因为登录凭证其实就是session的Id,就在Cookie中。
Cookie带着JSESSIONID到达服务端,服务端只需要基于sessionId得到session,再从session中取出用户,判断一下这个用户是否存在,如果存在说明登录成功,将用户返回给前端即可。
听起来很简单,但事实上这么做有些问题:在黑马点评的项目中有很多很多的controller,其中我们讲到的 /user/me
登录校验,它是 UserController
中的,前端向 UserController
发请求,你在UserController的对应业务中,编写右图一堆的业务逻辑:获取SessionId、获取session、获取用户、判断等等,但是后续随着业务的开发,越来越多的业务都需要去校验用户的登录,难道说在每一个controller里都来写这一堆的业务逻辑吗?太麻烦了。
在SpringMVC中有一个东西可以在所有的controller执行之前去做,即拦截器。有了拦截器后,用户的请求就不会直接访问到controller了,都必须先经过拦截器,再由拦截器判断该不该放行让你到达controller。
有了它后,我们可以将用户校验的登录流程都扔到拦截器中做,这样一来所有的controller都可以不用写这种校验了。
但是这里还存在一个问题:拦截器确实可以帮我们实现对用户的校验,但是校验完后,在后续的业务中却拿不到用户信息,因为只有在拦截器中根据session可以拿到用户信息。因此我们需要有一种方案需要将拦截器中拦截得到的用户信息传递到controller中去,而且在传递的过程中要注意线程的安全问题,此时就需要使用 ThreadLocal
来解决。
温馨小贴士:tomcat的运行原理
当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应
通过以上讲解,我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据。
总结:ThreadLocal是一个线程的对象,每一个进入Tomcat的请求都是一个独立的线程,将来ThreadLocal会在线程内为你开辟一个内存的空间去保存对应的用户,这样一来每个线程相互不干扰,因此不同用户访问同一个controller,都会有独立的线程,大家都有自己的用户信息互不干扰。因此我们会通过这个方式来实现整个校验流程。
温馨小贴士:关于threadlocal
如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离。
二、代码实现
1)拦截器代码
preHandle方法:目标资源方法(controller里的)执行前执行。 返回true:放行 返回false:不放行
postHandle方法:目标资源方法执行后执行
afterCompletion方法:视图渲染完毕后执行,最后执行
这里要实现 preHandle
、afterCompletion
,因为在进入controller之前肯定要做登录校验,用户业务执行完毕,还需要去销毁用户信息,避免内存泄漏。
并且ThreadLocal不需要大家写了,在Utils中定义了UserHolder类,ThreadLocal的泛型是User,也就是说只能存储User。
UserHolder.java
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
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();
}
}
2)自定义拦截器
- 实现HandlerInterceptor接口,并重写其所有方法
这三个方法都有默认实现,根据需要重写
快捷键:ctrl + O
- 定义好后加上@Component,需要交给IOC容器管理
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取session
HttpSession session = request.getSession();
//2.获取session中的用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if(user == null){
//4.不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//5.存在,保存用户信息到Threadlocal
// 由于前面获取到的是Object类型,因此这里需要强转
UserHolder.saveUser((User)user);
//6.放行
return true;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
3)注册配置拦截器
- 实现WebMvcConfigurer接口
- 加上@Configuration来表示当前就是spring当中的配置类
- 重写addInterceptors方法,在里面指定并注册拦截器,并且指定拦截器的拦截路径
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
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 {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**", // 优惠券信息
"/shop-type/**", // 店铺类型
"/upload/**", // 这里栏,但为了测试这里就不栏了
"/blog/hot", // 查热点博客,跟登录用户无关
"/user/code",
"/user/login"
);
}
}