在分布式的环境下,使用session会出现共享数据的问题。那么可以将共享数据存入数据库中,然后应用服务器就可以去数据库获取共享数据。对于每一次请求,可以在一开始从数据库里取到数据,然后将其临时存放在本地的内存里。考虑到线程安全的问题,所以使用threadlocal进行线程隔离,这样在本次请求的过程中,就可以随时获取到这份共享数据了。所以,session的替代方案是数据库,ThreadLocal在这里起辅助的作用。
这里演示偷懒直接用的mysql,推荐使用redis作为替代,否则mysql数据库压力会很大
先搞一个用于存储用户信息的threadLocal
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
设计是在登录后将用户凭证存到了cookie中,所以制作一个cookie工具类
public class CookieUtil {
public static String getValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
throw new IllegalArgumentException("参数为空!");
}
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
然后就可以制作拦截器
第一个是在方法请求前看有无用户凭证
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取凭证
String ticket = CookieUtil.getValue(request,"ticket");
if(ticket!=null){
LoginTicket loginTicket = userService.findLoginTicket(ticket);
//loginTicket是否为空,status为0代表还没退出登录,看过期时间是不是在现在的时间之后
if(loginTicket!=null&&loginTicket.getStatus()==0&&loginTicket.getExpired().after(new Date())){
//条件都成立就放到threadLoccal中
User user = userService.findUserById(loginTicket.getUserId());
hostHolder.setUser(user);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
//将用户信息放到modelAndView中
if(user!=null&&modelAndView!=null){
modelAndView.addObject("loginUser",user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//记得用完后清除一下threadLocal,否则可能内存泄露
hostHolder.clear();
}
}
第二个拦截器用于判断用户调用方法时是否登录。具体是看方法上是否是用了注解,以及用户是否处于登陆状态
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired annotation = method.getAnnotation(LoginRequired.class);
//看方法上有没有用到LoginRequired注解,以及用户是否为空,有用到且为空则代表用户没登陆,返回登录页你
if(annotation!=null&&hostHolder.getUser()==null){
response.sendRedirect(request.getContextPath()+"/login");
return false;
}
}
return true;
}
}
判断有无登录的注解,这里不需要写任何具体内容,仅在方法上作标识作用,以供拦截器识别
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
使拦截器生效
注意细节,必须先注册loginTicketInterceptor,再注册loginRequiredInterceptor,因为两边同时都使用了preHandle()方法,执行顺序会有影响
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}