Skip to content

什么是拦截器

  • 什么是拦截器:在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。拦截是AOP的一种实现策略

  • 为什么需要拦截器:在做身份认证或者是进行日志的记录时,我们需要通过拦截器达到我们的目的。最常用的登录拦截、或是权限校验、或是防重复提交、或是根据业务像12306去校验购票时间,总之可以去做很多的事情

  • 如何用拦截器:在spring中用拦截器需要实现HandlerInterceptor接口或者它的实现子类:HandlerInterceptorAdapter,同时在applicationContext.xml文件中配置拦截器

自定义拦截器

配置yml

yml
# 系统配置
sys-config:
  #操作项目根路径
  file-def-path: D:\
  # 拦截器-添加路径
  filter-add-path:
      # 静态资源
    - /**
    # 排除路径
  filter-excludes-path:
      # 静态资源
    - /

配置映射类

java

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @ClassName SystemConfig
 * @Description TODO
 */
@Data
@Component
@ConfigurationProperties(prefix = "sys-config")
public class SystemConfig {

    /**
     * 文件路径
     */
    private String fileDefPath;

    /**
     * 拦截器拦截路径
     */
    private String[] filterAddPath;

    /**
     * 拦截器排除路径
     */
    private String[] filterExcludesPath;

}

后端拦截器

  • 新建 SystemeFilter
java


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.example.annotation.AuthLoginAnnotation;
import lombok.extern.slf4j.Slf4j;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @ClassName SystemeFilter
 * @Description TODO
 */
@Slf4j
@Configuration
@Order(100) //值越小优先级越高
public class SystemeFilter implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        log.info("request请求地址path[{}] uri[{}] token[{}]", request.getServletPath(), request.getRequestURI(), request.getHeader("token"));

        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        AuthLoginAnnotation authLoginAnnotation = method.getAnnotation(AuthLoginAnnotation.class);
        if (authLoginAnnotation == null) {
            log.error("后端API,访问缺少注解配置!");
            return false;
        } else {
            if (!authLoginAnnotation.login()) {
                // 值为:false时 不进行登录验证 放行
                return true;
            } else {
                //进行登录验证
                String token = request.getHeader("token");// 从 http 请求头中取出 token
                if (token == null || "".equals(token)) {
                    token = request.getHeader("Authorization");
                    if (token == null || "".equals(token)) {
                        token = BeanUtil.copyProperties(request.getParameter("token"),String.class);
                    }
                }
                if (authLoginAnnotation.object().equals("admin")) {
                    if (token == null) {
                        response.setStatus(404);
                        log.error("缺少token,未登录");
                        throw new Exception("缺少Token,未登录");
                    } else {
                        final JWT jwt = JWTUtil.parseToken(token);
                        if (jwt == null) {
                            response.setStatus(404);
                            log.error("token认证失败");
                            //这里的异常是我自定义的异常,系统抛出异常后框架捕获异常然后转为统一的格式返回给前端, 其实这里也可以返回false
                            throw new Exception("无效Token");
                        } else {
                            //登录成功
                            //log.error("token认证成功");
                            return true;
                        }
                    }
                }
                return false;
            }
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

应用端拦截器

  • 新建AppletFilter
java
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * 自定义-应用程序拦截器
 */
@Slf4j
@Configuration
@Order(2)
public class AppletFilter implements HandlerInterceptor {

    /**
     * 预处理回调方法,实现处理器的预处理
     * 返回值:true表示继续流程;false表示流程中断,不会继续调用其他的拦截器或处理器
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        boolean annotationPresent = method.isAnnotationPresent(PassToken.class);

        //如果有 @PassToken  不进行验证
        if (annotationPresent) {
            return true;
        }
        String userToken = request.getHeader(SystemConstants.HEADER_STRING);
        String platformId = request.getHeader(SystemConstants.platformId);
        if (StringUtils.isBlank(userToken) || !userToken.startsWith(SystemConstants.TOKEN_PREFIX)) {
            log.error("小程序没有登录拦截了呦");
            throw new NotLogException("小程序没有登录拦截了呦");
            //return false;
        } else {
            final String token = userToken.substring(SystemConstants.TOKEN_PREFIX.length());
            try {
                
                WxUserViewModel wxUser = TokenUtil.verifyTokenAndGetUserInfo(token);
                if (wxUser == null) {
                    // throw new NotLogException();
                    logger.error("小程序没有登录拦截了呦");
                    throw new NotLogException("小程序没有登录拦截了呦");
                }
                //转发 设置请求参数
                request.setAttribute(SystemConstants.LOGIN_KEY, wxUser);
            } catch (final Exception e) {
                //
                logger.error("小程序没有登录拦截了呦");
                throw new NotLogException("小程序没有登录拦截了呦");
            }
        }
        return true;
    }

    /**
     * 后处理回调方法,实现处理器(controller)的后处理,但在渲染视图之前
     * 此时我们可以通过modelAndView对模型数据进行处理或对视图进行处理
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // TODO Auto-generated method stub
    }

    /**
     * 整个请求处理完毕回调方法,即在视图渲染完毕时回调,
     * 如性能监控中我们可以在此记录结束时间并输出消耗时间,
     * 还可以进行一些资源清理,类似于try-catch-finally中的finally,
     * 但仅调用处理器执行链中
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // TODO Auto-generated method stub
    }


}

注册拦截器

  • 新建RegisterFilter
java

import com.example.config.SystemConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @ClassName RegisterFilter
 * @Description TODO
 */
@Configuration
@RequiredArgsConstructor
public class RegisterFilter extends WebMvcConfigurationSupport {


    private final SystemConfig systemConfig;

    /**
     * 注册拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        //拦截器一
        registry.addInterceptor(new SystemeFilter()).
                //排除路径
                excludePathPatterns(systemConfig.getFilterExcludesPath()).
                //拦截路径
                addPathPatterns(systemConfig.getFilterAddPath());
       //拦截器二 同理添加其它拦截器
    }


    /**
     * 静态资源处理
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");

        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/", "classpath:/public/");

        super.addResourceHandlers(registry);
    }

常用拦截器方法

java
    /** 解决跨域问题 **/
    public void addCorsMappings(CorsRegistry registry) ;
    
    /** 添加拦截器 **/
    void addInterceptors(InterceptorRegistry registry);
    
    /** 静态资源处理 **/
    void addResourceHandlers(ResourceHandlerRegistry registry);

拦截器方法

java
configurePathMatch:配置路由请求规则
configureContentNegotiation:内容协商配置
configureAsyncSupport
configureDefaultServletHandling:默认静态资源处理器
addFormatters:注册自定义转化器
addInterceptors:拦截器配置
addResourceHandlers:资源处理
addCorsMappings:CORS配置
addViewControllers:视图跳转控制器
configureViewResolvers:配置视图解析
addArgumentResolvers:添加自定义方法参数处理器
addReturnValueHandlers:添加自定义返回结果处理器
configureMessageConverters:配置消息转换器。重载会覆盖默认注册的HttpMessageConverter
extendMessageConverters:配置消息转换器。仅添加一个自定义的HttpMessageConverter.
configureHandlerExceptionResolvers:配置异常转换器
extendHandlerExceptionResolvers:添加异常转化器
getValidator
getMessageCodesResolver

拦截器不起作用问题

  • 继承 WebMvcConfigurationSupport ,重写 addInterceptors 方法的类需用到 @Configuration 注解

  • addPathPatterns中的问题。拦截器最后路径一定要 /, 如果是目录的话则是 /*/

配置addCorsMappings跨域无效

这里是因为拦截器的执行顺序,优先级的问题 造成跨域配置没有在拦截器之前注入容器

解决方案 使用 corsFilter拦截器,保证优先进入CorsFilter控制逻辑,保证跨域请求参数到达

java

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;


/**
 * @ClassName CorsFilter
 * @Description TODO
 */
@Configuration
public class CorsConfig  {

    @Bean
    public FilterRegistrationBean corsFilter() {

        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true); // 允许cookies跨域
        config.addAllowedOrigin("*");// #允许向该服务器提交请求的URI,*表示全部允许,自定义可以添加多个,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        config.addAllowedHeader("*");// #允许访问的头信息,*表示全部,可以添加多个
        config.setMaxAge(1800L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.addAllowedMethod("*");// 允许提交请求的方法,*表示全部允许,一般OPTIONS,GET,POST三个够了
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);//对所有接口都有效
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(Ordered.LOWEST_PRECEDENCE); // 优先级最高
        return bean;

    }

}

用到的一些扩展类

多功能权限注解

  1. AuthLoginAnnotation
java
import java.lang.annotation.*;

/**
 * @author :shanpegnian - http://www.sxpcwlkj.com
 * @version :1.0
 * @description : com.sxpcwlkj.a_common.annotation
 * @date : 2019/3/9
 */

@Documented //文档生成时,该注解将被包含在javadoc中,可去掉
@Target(ElementType.METHOD)//目标是方法
@Retention(RetentionPolicy.RUNTIME) //注解会在class中存在,运行时可通过反射获取
@Inherited
public @interface AuthLoginAnnotation {
    /**
     * 检查是否已登录(注解的参数)
     * @return true-检查;默认true 进行登录验证
     */
    boolean login() default true;

    /**
     * 验证对象  user   webmember  app  ....
     *  默认拦截 后台 user对象
     * @return
     */
    String object() default "user";    //1:user:后台 2:pclm:pc联盟:3:pcgw:pc官网 4:pcjfg:5:pc集福购  6:app:app  7:xcx:小程序  8:gzh:公众号

    /**
     *  是否验证当前对象 访问权限
     *  默认 true  进行验证
     * @return
     */
    boolean authority() default true;

    /**
     *  权限标识
     *  默认 null  拦截所有
     * @return
     */
    String authorityCode() default "";

    /**
     * app是否验证key
     * 默认不验证
     * @return
     */
    boolean key() default false;
}

/*
注解方法不能有参数。
注解方法的返回类型局限于原始类型,字符串,枚举,注解,或以上类型构成的数组。
注解方法可以包含默认值。
注解可以包含与其绑定的元注解,元注解为注解提供信息,有四种元注解类型:
  1. @Documented – 表示使用该注解的元素应被javadoc或类似工具文档化,它应用于类型声明,类型声明的注解会影响客户端对注解元素的使用。如果一个类型声明添加了Documented注解,那么它的注解会成为被注解元素的公共API的一部分。

  2. @Target – 表示支持注解的程序元素的种类,一些可能的值有TYPE, METHOD, CONSTRUCTOR, FIELD等等。如果Target元注解不存在,那么该注解就可以使用在任何程序元素之上。

  3. @Inherited – 表示一个注解类型会被自动继承,如果用户在类声明的时候查询注解类型,同时类声明中也没有这个类型的注解,那么注解类型会自动查询该类的父类,这个过程将会不停地重复,直到该类型的注解被找到为止,或是到达类结构的顶层(Object)。

  4. @Retention – 表示注解类型保留时间的长短,它接收RetentionPolicy参数,可能的值有SOURCE(源文件中起作用), CLASS, 以及RUNTIME(保留到运行时起作用)。
*/
  1. PassToken
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author http://www.sxpcwlkj.com
 * @version 1.0
 * @description com.lhym.applet.config
 * @date 2020/11/2
 * 自定义注解  用来跳过验证的PassToken
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}
  1. NoAuthorityException
java
public class NoAuthorityException extends Exception{

    private static final long serialVersionUID = 1L;

    public NoAuthorityException(String msg) {
        super(msg);
    }

    public NoAuthorityException(String msg,Throwable cause){
        super(msg,cause);
    }
    public NoAuthorityException(Throwable cause){
        super(cause);
    }

}
  1. TokenUtils

verifyTokenAndGetUserInfo 主要是从请求头把Token拿到进行解析,然后根据自定义的Token生成方法,生成一个 用户登录对象 getpareJwt 主要是从请求头把Token拿到进行解析,拿到解析的所有参数

验证

访问URL:/admin/user/imageCodeEx ,/admin/user/userLogin , /api/order/info , /api/live/room

放行

访问URL:/admin/index

走后端拦截器

访问URL:/api/index

走应用端拦截器

Released under the MIT License.