过滤器与拦截器
一、前言
常用项目编写规范参考:
1、概述
前面讲到数据统一响应、全局异常等常用后端框架,那么随着项目的开发,需要对请求进行校验(参数校验、前面校验等),不符合的不进入后端业务逻辑,提前返回并抛出异常。一般实现方法有拦截器和过滤器,这两者都可以实现对应的功能,可以根据自己喜好进行编写。
过滤器一般完成通用的操作。如:登录验证、统⼀编码处理、敏感字符过滤,常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件等;拦截器采用AOP的设计思想, 它跟过滤器类似, 用来拦截处理方法在之前和之后执行一些 跟主业务没有关系的一些公共功能,比如权限控制、日志、异常记录、记录方法执行时间
下面来讲讲这两者的异同和代码demo。
2、过滤器与拦截器异同
2.1 简介
过滤器(filter)和拦截器(Inteceptor)的执行顺序概览
2.2 异同
-
过滤器和拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,因为拦截器是spring提供并管理的,spring的功能可以被拦截器使用,在拦截器里注入一个service,可以调用业务逻辑。而过滤器是JavaEE标准,只需依赖servlet api ,不需要依赖spring 过滤器的实现基于回调函数。而拦截器(代理模式)的实现基于反射 Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用 Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理(反射)的方式来执行 Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便
2.3 总结
-
过滤器可以修改request,而拦截器不能 过滤器需要在servlet容器中实现,拦截器可以适用于javaEE,javaSE等各种环境 拦截器可以调用IOC容器中的各种依赖,而过滤器不能 过滤器只能在请求的前后使用,而拦截器可以详细到每个方法
具体的执行调用流程如下
-
过滤器(Filter) :可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息 拦截器(Interceptor):可以拿到你请求的控制器和方法,却拿不到请求方法的参数 切片(Aspect): 可以拿到方法的参数,但是却拿不到http请求和响应的对象
这里说一下为什么spring security使用过滤器而不是拦截器。因为作为一个通用的安全框架不应该耦合其他web框架的元素。很显然拦截器是spring mvc或struts等框架提供的,如果基于拦截器势必耦合这些框架,就做不到通用了
3、Filters vs HandlerInterceptors
-
Filter 是 Servlet 规范中的,而 HandlerInterceptor 是 Spring 中的一个概念 拦截器位置相对于过滤器更靠后 精细的预处理任务适用于拦截器,如授权检查等 内容处理相关或通用的流程,非常适合用过滤器;如上传表单、zip 压缩、图像处理、日志记录请求、身份验证等 HandlerInterceptor 的 postHandle 方法允许我们向视图添加更多模型对象,但不能更改 HttpServletResponse,因为它已经被提交了 过滤器的 doFilter 方法比拦截器的 postHandle 更通用。我们可以在过滤器中改变请求或响应,并将其传递给链,甚至阻止请求的处理 HandlerInterceptor 提供了比过滤器更精细的控制,因为我们可以访问实际的目标 handler,甚至可以检查 handler 方法是否有某个特定的注解
二、过滤器
1、概述
过滤器(Filter)是处于客户端与服务器目标资源之间的⼀道过滤技术,当访问服务器的资源时,过滤器可以将请求拦截下来,完成⼀些特殊的功能
执行是在Servlet之前,客户端发送请求时,会先经过Filter,再到达目标Servlet中;响应时, 会根据执行流程再次反向执行Filter,⼀般用于完成通用的操作。如:登录验证、统⼀编码处理、敏感字符过滤。常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件等
2、生命周期
2.1 生命周期概述
过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。
-
init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。注意:这个方法必须执行成功,否则过滤器会不起作用 doFilter() :容器中的每一次请求都会调用该方法,比如定义一个 Filter 拦截 /path/*,那么每一个匹配 /path/* 访问资源的请求进来时,都会执行此方法, FilterChain 用来调用下一个过滤器 Filter。不同的过滤器通过@Order()排序注解执行顺序 destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次
2.2 基于函数回调实现原理
在我们自定义的过滤器中都会实现一个 doFilter()方法,这个方法有一个FilterChain 参数,而实际上它是一个回调接口。ApplicationFilterChain是它的实现类, 这个实现类内部也有一个 doFilter() 方法就是回调方法。
public interface FilterChain { void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException; }
ApplicationFilterChain里面能拿到我们自定义的xxxFilter类,在其内部回调方法doFilter()里调用各个自定义xxxFilter过滤器,并执行 doFilter() 方法
public final class ApplicationFilterChain implements FilterChain { @Override public void doFilter(ServletRequest request, ServletResponse response) { ...//省略 internalDoFilter(request,response); } private void internalDoFilter(ServletRequest request, ServletResponse response){ if (pos < n) { //获取第pos个filter ApplicationFilterConfig filterConfig = filters[pos++]; Filter filter = filterConfig.getFilter(); ... filter.doFilter(request, response, this); } } }
而每个xxxFilter 会先执行自身的 doFilter() 过滤逻辑,最后在执行结束前会执行filterChain.doFilter(servletRequest, servletResponse),也就是回调ApplicationFilterChain的doFilter() 方法,以此循环执行实现函数回调
3、自定义过滤器两种实现方式
不论是注解配置还是Java配置,都需要在启动类上加上@ServletComponentScan("过滤器路径")注解,过滤路径可以不写(或者直接注入容器交给spring管理)。注解注册和Java配置类注册,它们的自定义过滤器类都是一样的,只不过注册过程一个是通过@WebFilter注解,一个是通过Java配置类注册Bean。
3.1 @WebFilter注解注册
/** * 自定义注解过滤器实现 * Filter的包是javax.servlet.Filter的 * filterName:过滤器名称,需要唯一,不能重复 * urlPatterns:要拦截的url资源路径,注意:通配符是一个星号(*) */ @Order(2)//排序注解,执行顺序 @WebFilter(filterName = "filterAnnotation",urlPatterns = { "/study/interfaces/v1/user"}) public class filterAnnotation implements Filter { //初始化操作,只会执行一次 @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("filterAnnotation--初始化Filter"); } //进入到过滤资源之前和之后做的事情 @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("filterAnnotation--进入Target Resource之前做的事情"); filterChain.doFilter(servletRequest,servletResponse); System.out.println("filterAnnotation--处理返回的Response"); } //销毁,只会在项目停止或者重新部署的时候才会执行 @Override public void destroy() { System.out.println("filterAnnotation--销毁Filter"); } }
再举一个例子
/** * 检查用户是否已经完成登录 */ @WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*") @Slf4j public class LoginCheckFilter implements Filter{ //路径匹配器,支持通配符 public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; //1、获取本次请求的URI String requestURI = request.getRequestURI();// /backend/index.html log.info("拦截到请求:{}",requestURI); //定义不需要处理的请求路径 String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**", "/common/**" }; //2、判断本次请求是否需要处理 boolean check = check(urls, requestURI); //3、如果不需要处理,则直接放行 if(check){ log.info("本次请求{}不需要处理",requestURI); filterChain.doFilter(request,response); return; } //4、判断登录状态,如果已登录,则直接放行 if(request.getSession().getAttribute("employee") != null){ log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee")); Long empId = (Long) request.getSession().getAttribute("employee"); BaseContext.setCurrentId(empId); filterChain.doFilter(request,response); return; } log.info("用户未登录"); //5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据 response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return; } /** * 路径匹配,检查本次请求是否需要放行 */ public boolean check(String[] urls,String requestURI){ for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if(match){ return true; } } return false; } }
3.2 过滤器(配置类注册过滤器)
public class BaseFilter implements Filter { Logger logger = LoggerFactory.getLogger(BaseFilter.class); static final String TOKEN = "20220423344556abac"; //内部接口集合 public static List<String> INSIDE_URLS = Lists.newArrayList("/index","/inside"); //白名单接口集合 public static List<String> WHITE_PATH = Lists.newArrayList("/white","/login"); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("初始化数据"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper((HttpServletResponse)servletResponse); HttpServletRequest request = (HttpServletRequest) servletRequest; String requestURI = request.getRequestURI(); if(INSIDE_URLS.contains(requestURI)){ //内部接口,直接通过 filterChain.doFilter(servletRequest,servletResponse); return; } if(WHITE_PATH.contains(requestURI)){ //白名单接口,直接通过 filterChain.doFilter(servletRequest,servletResponse); return; } //进行校验,如token校验 String token = request.getHeader("token"); if(TOKEN.equals(token)){ filterChain.doFilter(servletRequest,servletResponse); }else { //token校验不通过,重定向到登录页面 wrapper.sendRedirect("/login"); } } @Override public void destroy() { } }
然后设置配置类
/** * 作用相当于@WebFilter这个注解 * 过滤器配置类,进过滤器配置到bean中 * Filter的包是javax.servlet.Filter的 */ @Configuration//这个注解的目的是被IOC容器获取到 public class FilterConfig { /** * 基础过滤器 * @return */ @Bean public FilterRegistrationBean<Filter> baseFilter(){ FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new BaseFilter());//注册自定义过滤器类 //过滤资源的路径,或者静态资源,注意:通配符是一个星号(*) filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/*")); filterRegistrationBean.setOrder(1);//排序 return filterRegistrationBean; } }
4、实战OncePerRequestFilter
自定义配置类
@Data @Configuration @ConfigurationProperties(prefix = "security.checker") public class SecurityCheckerConfig { private Boolean enable; /** * 存放accessKey和accessSecurity */ private Map<String, String> maps; /** * sign的过期时间 */ private Integer signExpireTime; }
下面继承了OncePerRequestFilter 来实现我们自己的自定义过滤器,OncePerRequestFilter 特点是请求进入后只会过滤一次,不会重复过滤(有些情况请求可能会两次进入相同的过滤器),同时在不符合要求的请求需要即使抛出异常返回,或者重定向到其他接口
@Component @Slf4j public class ParamCheckFilter extends OncePerRequestFilter { @Autowired @Qualifier("handlerExceptionResolver") private HandlerExceptionResolver resolver; // 自己在application.yaml定义的字段 @Resource private SecurityCheckerConfig securityCheckerConfig; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if(!securityCheckerConfig.getEnable()){ filterChain.doFilter(request,response); return; } String timestamp = request.getHeader("timestamp"); String accessKey = request.getHeader("accesskey"); String sign = request.getHeader("sign"); //根据key在配置文件拿取accessSecret String accessSecret = securityCheckerConfig.getMaps().get(accessKey); //检查时间戳合法性 if(!StringUtils.isNumeric(timestamp)){ // 异常类自定义的 resolver.resolveException(request,response,null,new TruckException(CustomCodeEnum.TIMESTAMP_IS_WRONGFUL)); return; } //禁止超时签名 Long ts = Long.valueOf(timestamp); if (System.currentTimeMillis() - ts > (securityCheckerConfig.getSignExpireTime() * CommonConstant.SECOND_TO_MILLIS)) { resolver.resolveException(request,response,null,new TruckException(CustomCodeEnum.SIGN_OVERTIME)); return; } // 检查KEY是否合理 if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(accessSecret)) { resolver.resolveException(request,response,null,new TruckException(CustomCodeEnum.ACCESSKEY_WRONGFUL)); return; } if(!checkSign(getBody(request),accessSecret, sign)){ resolver.resolveException(request,response,null,new TruckException(CustomCodeEnum.SIGN_ERROR)); return; } filterChain.doFilter(request,response); } private boolean checkSign(Map<String, Object> params, String accessSecret, String originSign) { String sign = createSign(params, accessSecret); if (!sign.equals(originSign)) { log.error("sign 校验不通过! params: {}, ours sign : {}, theirs : {}", params, sign, originSign); return false; } return true; } /** * 获取请求体(去除空值) */ private LinkedHashMap<String, Object> getBody(HttpServletRequest request) { Map<String, String[]> requestParameterMap = request.getParameterMap(); JSONObject params = new JSONObject(); if(!CollectionUtils.isEmpty(requestParameterMap)){ requestParameterMap.forEach((k,v) -> params.put(k,v[0])); } return sortFields(params); } /** * 将请求参数按照ASCII码排序,方便校验sign */ private LinkedHashMap<String, Object> sortFields(JSONObject params) { // 将请求参数按照ASCII码排序,方便校验sign String json = JSON.toJSONString(params, SerializerFeature.SortField); return JSONObject.parseObject(json, LinkedHashMap.class, Feature.OrderedField); } /** * 生成sign * @param params 所有字段按照ASCII码排序,否则签名不一样 */ public String createSign(Map<String, Object> params, String accessSecret) { Set<String> keysSet = params.keySet(); Object[] keys = keysSet.toArray(); Arrays.sort(keys); // 拼接所有一级字段,二级字段不处理,但是字段按ASCII码排序 List<String> paramList = new ArrayList<>(); for (Object key : keys) { String value = String.valueOf(params.get(key)); String str = key + "=" + value.replaceAll("["| ]","").replaceAll(":", "").trim(); paramList.add(str); } paramList.add("accessSecret=" + accessSecret); String paramStr = String.join("&", paramList); return DigestUtils.md5DigestAsHex(paramStr.getBytes()).toUpperCase(); // return DigestUtils.md5Hex(paramStr).toUpperCase(); } }
这里要注意的是我们用了HandlerExceptionResolver ,因为在Spring Boot由于全局异常处理@RestControllerAdvice只会去捕获所有Controller层抛出的异常,所以在filter当中抛出的异常GlobalExceptionHandler类是没有感知的,所以在filter当中抛出的异常最终会被Spring框架自带的全局异常处理类BasicErrorController捕获,会返回基础格式的Json响应
一种方法是继承上面所说的BasicErrorController类,并重写error()方法;另一种就是在filter当中引入HandlerExceptionResolver类,通过该类的resolveException方法抛出自定义异常,通过resolveException方法抛出的自定义异常可以被RestControllerAdvice捕获,从而满足我们的需求,最终得到的响应格式
三、拦截器
1、概述
拦截器采用AOP的设计思想, 它跟过滤器类似, 用来拦截处理方法在之前和之后执行一些 跟主业务没有关系的一些公共功能,比如可以实现权限控制、日志、异常记录、记录方法执行时间等等
2、自定义拦截器
2.1 生命周期
SpringMVC提供了拦截器机制,允许运行目标方法之前进行一些拦截工作或者目标方法运行之后进行一下其他相关的处理。自定义的拦截器必须实现 HandlerInterceptor接口。
HandlerInterceptor 接口中定义了三个方法
-
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) :这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。 postHandle(HttpServletRequest request, HttpServletResponse response, Object handler):只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler):只有在 preHandle() 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try catch finally中的finally,但仅调用处理器执行链中preHandle返回true的拦截器才会执行
2.2 代码示例
首先创建自定义拦截器
public class MyFirstInterceptor implements HandlerInterceptor { /** * 在处理方法之前执 日志、权限、 记录调用时间 * @param request 可以在方法请求进来之前更改request中的属性值 * @param response * @param handler 封装了当前处理方法的信息 * @return true 后续调用链是否执行/ false 则中断后续执行 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在请求映射到对应的处理方法映射,实现类才是HandlerMethod。 // 如果是视图控制器,实现类ParameterizableViewController if(handler instanceof HandlerMethod ) { HandlerMethod handMethod = (HandlerMethod) handler; } /*System.out.println("-------类["+handMethod.getBean().getClass().getName()+"]" + "方法名["+handMethod.getMethod().getName()+"]" + "参数["+ Arrays.toString(handMethod.getMethod().getParameters()) +"]前执行--------preHandle");*/ System.out.println(this.getClass().getName()+"---------方法后执行,在渲染之前--------------preHandle"); return true; } /** * 如果preHandle返回false则会不会允许该方法 * 在请求执行后执行, 在视图渲染之前执行 * 当处理方法出现了异常则不会执行方法 * @param request * @param response 可以在方法执行后去更改response中的信息 * @param handler 封装了当前处理方法的信息 * @param modelAndView 封装了model和view.所以当请求结束后可以修改model中的数据或者新增model数据,也可以修改view的跳转 * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println(this.getClass().getName()+"---------方法后执行,在渲染之前--------------postHandle"); } /** * 如果preHandle返回false则会不会允许该方法 * 在视图渲染之后执行,相当于try catch finally 中finally,出现异常也一定会执行该方法 * 如果执行的时候核心的业务代码出问题了,那么已经通过的拦截器的 afterCompletion会接着执行 * @param ex Exception对象,在该方法中去做一些:记录异常日志的功能,或者清除资源 * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println(this.getClass().getName()+"---------在视图渲染之后--------------afterCompletion"); } }
然后在spring boot 项目中配置,实现 WebMvcConfigurer 接口 并重写 addInterceptors方法
@Configuration public class InterceptorAdapter implements WebMvcConfigurer { @Bean public MyFirstInterceptor myInterceptor(){ return new MyFirstInterceptor(); } public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/*.html"); } } @RequestMapping("/test01") public String test01(){ System.out.println("请求方法执行中..."); return "admin"; } }
2.3 多拦截器示例
拦截顺序取决于配置的顺序
@Configuration public class InterceptorAdapter implements WebMvcConfigurer { @Bean public MyFirstInterceptor myInterceptor(){ return new MyFirstInterceptor(); } @Bean public MySecondInterceptor mySecondInterceptor(){ return new MySecondInterceptor(); } public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor()).addPathPatterns("/**").excludePathPatterns("/*.html"); registry.addInterceptor(mySecondInterceptor()).addPathPatterns("/**") .excludePathPatterns("/*.html") .order(1); } }
看到输出结果发现,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。但注意postHandle() 方法被调用的顺序跟 preHandle() 是相反的。我们要知道controller 中所有的请求都要经过核心组件DispatcherServlet路由,都会执行它的 doDispatch() 方法,而拦截器postHandle()、preHandle()方法便是在其中调用的。查看源码可知,发现两个方法中在调用拦截器数组 HandlerInterceptor[] 时,循环的顺序竟然是相反的
3.4 静态资源被拦截问题
配置拦截器会导致静态资源被拦截,比如在 resources/static/ 目录下放置一个图片资源或者 html 文件,然后启动项目直接访问,即可看到无法访问的现象。也就是说,虽然 Spring Boot 2.0 废弃了WebMvcConfigurerAdapter,但是 WebMvcConfigurationSupport 又会导致默认的静态资源被拦截,这就需要我们手动将静态资源放开
除了在 MyInterceptorConfig 配置类中重写 addInterceptors 方法外,还需要再重写一个方法:addResourceHandlers,将静态资源放开
/** * 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无法直接访问 * @param registry */ @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**").addResourceLocations("classpath:/static/"); super.addResourceHandlers(registry); }
这样配置好之后,重启项目,静态资源也可以正常访问了。
另一种方法是不继承 WebMvcConfigurationSupport 类,直接实现 WebMvcConfigurer 接口,然后重写 addInterceptors 方法,将自定义的拦截器添加进去即可(上面讲到)
@Configuration public class MyInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 实现WebMvcConfigurer不会导致静态资源被拦截 registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**"); } }
4、实战demo
判断用户有没有登录,一般用户登录功能我们可以这么做,要么往 session 中写一个 user,要么针对每个 user 生成一个 token,第二种要更好一点,那么针对第二种方式,如果用户登录成功了,每次请求的时候都会带上该用户的 token,如果未登录,则没有该 token,服务端可以检测这个 token 参数的有无来判断用户有没有登录,从而实现拦截功能。我们改造一下 preHandle 方法
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); String methodName = method.getName(); logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName); // 判断用户有没有登陆,一般登陆之后的用户都有一个对应的token String token = request.getParameter("token"); if (null == token || "".equals(token)) { logger.info("用户未登录,没有权限执行……请登录"); return false; } // 返回true才会继续执行,返回false则取消当前请求 return true; }
最后还有一个监听器,可以参考: