参考牛客网高级项目教程
1. 显示登录信息功能需求
要实现的功能:
- 用户在未登录和登录两种状态,访问页面时,应该显示不同的头部信息
- 即,未登录状态:
- 访问主页,有登录、注册选项,没有个人信息状态展示
- 登录状态:
- 访问主页,没有登录、注册选项,显示个人信息状态
- 即,未登录状态:
为何使用拦截器及拦截器本质
-
策略一:如果在每个请求中进行处理,
- 会增大代码的重复工作量,每个请求中都要处理类似逻辑
- 耦合性也很高,每个请求中要引入其他请求中获取的用户信息
-
策略二:采用请求拦截器处理:
-
即,在每个请求前,请求后,模板渲染前、模板渲染后进行横向拦截
-
在拦截器中处理相关业务,不改变原有请求逻辑,减少耦合度和重复代码量
- 查询用户信息,
- 持有用户信息,
- 以及交给模板展示用户数据,
- 清理用户数据
-
拦截器的使用,本质是面向切面编程,使用了静态代理模式
-
2. 拦截器使用示例
2.1 设计拦截器实现类(代理角色)
- 即定义拦截的切入点
交给ioc管理
@Component
实现接口,重写方法
HandlerInterceptor
- 拦截器处理接口
preHandle
- 在Controller之前执行
- return true; 表示拦截处理之后继续执行,false就不往下进行了
- Object handler,拦截请求中的方法名
postHandle
- 在Controller之后,模板视图渲染前执行
afterCompletion
-
在模板视图渲染后处理,一般做清理工作
@Component public class AlphaInterceptor implements HandlerInterceptor { private Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class); // 在Controller之前执行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.debug("preHandle" + handler.toString()); return true; // true,表示拦截处理之后继续执行 } // 在Controller之后,模板视图渲染前执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { logger.debug("postHandle" + handler.toString()); } // 在模板视图渲染后处理,一般做清理工作 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { logger.debug("afterHandle" + handler.toString()); } }
2.2 配置-拦截哪些请求的哪些方法
- 即配置拦截的切面和通知方法
交给ioc管理
@Configuration
实现接口,重写方法
WebMvcConfigurer
registry.addInterceptor(拦截器类)
- 注入哪个拦截器类-定义切面
- 还是返回registry对象,方便连续调用其他函数
excludePathPatterns
- 配置拦截路径-排除哪些路径不需要拦截
addPathPatterns
-
添加需要拦截哪些url路径请求
-
不添加,默认除了排除的路径外所有请求路径
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired AlphaInterceptor alphaInterceptor; // 注入设置:注入哪个拦截器-拦截哪些路径(排除哪些路径,添加哪些路径) @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(alphaInterceptor) .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg") .addPathPatterns("/register", "/login"); // 不添加,默认除了排除的路径外所有请求路径 } }
测试结果:
-
不是在特定拦截路径下的请求:
- 如主页/index请求,没有相关日志打印信息
-
在特定拦截路径下的请求:
preHandlepublic java.lang.String com.nowcoder.community.controller.LoginController.getLoginPage() // 拦截请求中的方法名 postHandlepublic java.lang.String com.nowcoder.community.controller.LoginController.getLoginPage() afterHandlepublic java.lang.String com.nowcoder.community.controller.LoginController.getLoginPage()
3. 登录注册模块拦截器的实例
3.1 设计拦截器实现类
拦截器类要实现的功能需求
- 每个请求都需要被服务器验证cookie中的ticket
- 服务器会通过ticket查询用户信息,并将用户信息交给模板页面渲染
- 模板页面会根据是否有用户信息进行区别渲染显示-从而区分登录与未登录状态
所以,拦截器要增强请求的这方面功能,帮助每个请求实现这些功能
1)preHandle
- 目标是在controller处理请求前,要通过请求中传过来的ticket,查询到user信息
- 1.由于cookie中key很多,要找指定的key,需要设计一个工具类去获取
- 2.根据ticket查询到有效user用户信息
- 注意不能直接通过Ticket类查询用户信息,因为需要判断t票是否有效,登出也无效
- 只有t票有效,才能查询有效的用户信息
- 3.将user用户信息暂存在当前请求线程容器中,即持有用户
- 注意不能简单存储到一个类中,
- 因此多个请求对一个服务器,每个线程处理一个请求,涉及到多线程并发问题
- 因此,需要线程隔离,存到session中自动会线程隔离,但session中不适合分布式部署
- 因此,需要设计一个线程隔离的工具类hostHodel,使用ThreadLocal类实现线程隔离
- 保存在线程容器中,请求没结束前,线程一直活着,一直持有有效用户信息
- 注意不能简单存储到一个类中,
1.获取ticket值的cookie工具类设计
request.getCookies()
-
注意边界条件:判空处理,
- 不为空,遍历cookie数组,找到指定key的cookie
- 如果为空,返回值设为null,交给上级去判断处理
public class CookieUtil { public static String getValue(HttpServletRequest request, String name) { Cookie[] cookies = request.getCookies(); if(cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals(name)) { return cookie.getValue(); } } } return null; } }
2.根据ticket查询用户信息
-
在service层添加查询业务,只能查询到t票类,还有进一步验证t票是否有效
// 根据用户凭证ticket查询t票类 public LoginTicket selectByTicket(String ticket) { return loginTicketMapper.selectByTicket(ticket); }
.after(new Date()
-
验证t票是否有效,有效才查询有效用户信息
- t票不为空
- 状态为0,有效状态
- 有效时间在当前时间之后
// 2.根据ticket查询user if(ticket != null) { // 向数据库中查询凭证 LoginTicket loginTicket = userService.selectByTicket(ticket); if(loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { // 有效,就根据凭证找到用户信息 User user = userService.findUserById(loginTicket.getUserId()); // 在本次请求中持有用户信息,在请求结束前一直保存在请求的当前线程容器中 hostHolder.setUsers(user); } }
3.设计存储user的线程隔离工具类
ThreadLocal<>
- ThreadLocal:线程处理工具类,提供了线程安全的set,get方法
set(user)
-
往单个个线程都存user信息
- 先拿到当前线程,再往当前线程里存值
users.get()
-
先拿到当前线程,再从当前线程里取值
users.remove()
-
清理当前线程的值
-
总体代码实现:
@Component public class HostHolder { private ThreadLocal<User> users = new ThreadLocal<>(); // 往单个个线程都存user信息 // 先拿到当前线程,再往当前线程里存值 public void setUsers(User user) { users.set(user); } // 先拿到当前线程,再从当前线程里取值 public User getUser() { return users.get(); } // 同理,清理当前线程的user值 public void clear() { users.remove(); } }
2)postHandle
-
目标是用持有的用户信息,即Controller处理后,模板渲染前,将用户信息放到modelAndView中
-
模板页面根据持有的用户信息不同(是否有)区别渲染页面
// 在Controller之后,模板视图渲染前执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 在渲染前获取用户信息具体,进行渲染 User user = hostHolder.getUser(); if(user != null && modelAndView != null) { logger.debug("postHandle " + user.toString()); modelAndView.addObject("loginUser", user); } }
3)afterCompletion
-
渲染页面之后,即将渲染页面渲染显示给浏览器,这个请求处理完成后,将线程容器中的user信息清空
// 在模板视图渲染后处理,一般做清理工作 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { logger.debug("afterHandle " + handler.toString()); hostHolder.clear(); } }
3.2 拦截器配置
-
排除静态资源后,默认是所有项目下的请求路径
@Autowired LoginTicketInterceptor loginTicketInterceptor; // 注入设置:注入哪个拦截器-拦截哪些路径(排除哪些路径,添加哪些路径) @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginTicketInterceptor) .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg"); }
3.3 在模板页面区分处理携带user状态信息
th:if="${loginUser != null}"
- 根据请求线程中持有的user用户信息是否有,动态判断是否显示
- 消息:不为空才渲染
- 注册、登录: 为空才渲染
- 用户信息:不为空才渲染
- 将用户头像、用户名动态显示为线程中持有用户的信息
测试结果
-
登录前
-
登录后
-
退出登录后