过滤器与拦截器

news2024/9/27 7:17:37

文章目录

  • 一、前言
    • 1、概述
    • 2、过滤器与拦截器异同
      • 2.1 简介
      • 2.2 异同
      • 2.3 总结
    • 3、Filters vs HandlerInterceptors
  • 二、过滤器
    • 1、概述
    • 2、生命周期
      • 2.1 生命周期概述
      • 2.2 基于函数回调实现原理
    • 3、自定义过滤器两种实现方式
      • 3.1 @WebFilter注解注册
      • 3.2 过滤器(配置类注册过滤器)
    • 4、实战OncePerRequestFilter
  • 三、拦截器
    • 1、概述
    • 2、自定义拦截器
      • 2.1 生命周期
      • 2.2 代码示例
      • 2.3 多拦截器示例
      • 3.4 静态资源被拦截问题
    • 4、实战demo

一、前言

常用项目编写规范参考:Spring Boot后端接口规范

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 压缩、图像处理、日志记录请求、身份验证等
  • HandlerInterceptorpostHandle 方法允许我们向视图添加更多模型对象,但不能更改 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),也就是回调ApplicationFilterChaindoFilter() 方法,以此循环执行实现函数回调

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;
}

最后还有一个监听器,可以参考:Spring事件监听


https://zhuanlan.zhihu.com/p/340397290

https://zhuanlan.zhihu.com/p/484289805

https://www.zhihu.com/question/443466900/answer/2509838187

https://blog.csdn.net/qq_45534061/article/details/106266747

https://blog.csdn.net/m0_37731470/article/details/116754395

https://blog.csdn.net/xinzhifu1/article/details/106356958/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/354443.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue2vue3常用语法(持续更新)

一、基础1. 指令指令描述v-if判断v-else-if判断后剩下的v-else判断后剩下的v-html渲染html文本格式v-text渲染文本v-for循环v-showdisplay&#xff1a;none/block切换v-model双向绑定v-bind(缩写&#xff1a;:)动态绑定v-on(缩写&#xff1a;)绑定dom事件(如点击事件)v-cloak解…

WMS AMS【Android Framework进阶】

1.简介 可以毫不夸张的说&#xff0c;android的framework层主要是由WMS、AMS还有View所构成&#xff0c;这三个模块穿插交互在整个framework中&#xff0c;掌握了它们之间的关系和每一个逻辑步骤&#xff0c;你对framework的了解至少有百分之五十 AMS是Android中最核心的服务…

设计模式:行为型设计模式

参考文章&#xff1a; 《设计模式》 《设计模式知识体系详解》 《DesignPatterns》 写在开头&#xff1a;本文为学习后的总结&#xff0c;可能有不到位的地方&#xff0c;错误的地方&#xff0c;欢迎各位指正。 前言 行为型模式是将不同的行为代码解耦&#xff0c;从而解决…

疯狂弹出请插入多卷集的最后一张磁盘窗口

整个人嘛了&#xff0c;今天插上U盘&#xff0c;跟tmd中了病毒一样&#xff0c; 屏幕疯狂弹出窗口&#xff0c; 提示请插入多卷集的最后一张磁盘&#xff01; 点确定之后他继续弹出&#xff0c;点取消它也继续弹出&#xff0c; 关掉一个又弹出来一个&#xff0c;妈的&#x…

系统编程中的进程的概念No.3【进程状态】

引言&#xff1a; 北京时间&#xff1a;2023/2/17/8:17&#xff0c;目前听着超能陆战队主题曲《Immortals》&#xff0c;感觉又要螺旋式升天&#xff0c;并且为我今天上午没课感到happy&#xff0c;所以继我们很久以前的关于进程的博客&#xff0c;今天我们就再来学习一下有关…

buuctf Web 下

9.[ACTF2020 新生赛]Exec 访问url&#xff1a; http://cc3c6c27-e2df-4665-baba-1d9a32dc963e.node3.buuoj.cn/ 首页如下&#xff1a; 直接ping ip可以得到结果 常见管道符 1、|&#xff08;就是按位或&#xff09;&#xff0c;直接执行|后面的语句 127.0.0.1 | cat /flag…

html 的相对路径和绝对路径

整篇文章是以 src 标签进行演示。 文章目录 一、相对路径 1、同级目录查找 2、上一级目录查找 3、下一级目录查找 二、绝对路径 一、相对路径 &#x1f475;相对路径&#xff1a;从当前目录开始查找。 1、同级目录查找 写法&#xff1a; 1.1.直接写文件名字&#xff1b;…

Linux之进程控制

一.进程创建 1.1 fork函数 我们创建进程的方式有./xxx和fork()两种 在linux中fork函数时非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 #include <unistd.h> pid_t fork(void); 返回值&#xff1a;自进程…

Echarts实现多柱状图重叠重叠效果

有两种重叠效果: 1. 多个柱子重叠为一个 2. 多个柱子重叠为两组 第一种,图例: 这个灰色不是阴影哦, 是柱子. 1. 使用详解 (1) series.Z 折线图组件的所有图形的 z 值。控制图形的前后顺序。 z 值小的图形会被 z 值大的图形覆盖。z 相比 zlevel 优先级更低&#xff0c;而且不会…

GEE学习笔记 七十三:【GEE之Python版教程七】静态展示影像和动态展示影像

我们使用GEE在线编辑可以直接通过在线的网页可以加载展示我们计算的结果&#xff0c;而python版的GEE要展示我们的计算结果可能就比较麻烦。如果有同学看过GEE的python版API中可以找到一个类ee.mapclient&#xff0c;这个类的介绍是它是GEE官方通过Tk写的一个加载展示地图的类。…

【蓝桥日记⑤】2014第五届省赛(软件类)JavaA组❆答案解析

【蓝桥日记⑤】2014第五届省赛&#xff08;软件类&#xff09;JavaA组☃答案解析 文章目录【蓝桥日记⑤】2014第五届省赛&#xff08;软件类&#xff09;JavaA组☃答案解析1、猜年龄2、李白打酒3、神奇算式4、写日志5、锦标赛6、六角填数7、绳圈8、兰顿蚂蚁9、斐波那契10、波动…

Linux 操作系统原理 — NUMA 体系结构

目录 文章目录 目录NUMA 体系结构NUMA 的基本概念查看 Host 的 NUMA TopologyBash 脚本DPDK 脚步NUMA 体系结构 NUMA(Non-Uniform Memory Access,非一致性存储器访问)的设计理念是将 CPU 和 Main Memory 进行分区自治(Local NUMA node),又可以跨区合作(Remote NUMA nod…

操作系统 三(存储管理)

一、 存储系统的“金字塔”层次结构设计原理&#xff1a;cpu自身运算速度很快。内存、外存的访问速度受到限制各层次存储器的特点&#xff1a;1&#xff09;主存储器&#xff08;主存/内存/可执行存储器&#xff09;保存进程运行时的程序和数据&#xff0c;内存的访问速度远低于…

【信管12.2】知识管理与知识产权

知识管理与知识产权想必你对知识的概念多少都会有一些自己的理解&#xff0c;毕竟我们经过了那么多年的教育&#xff0c;学来学去可不都学习的是“知识”嘛。在今天的学习中&#xff0c;内容还是会比较多&#xff0c;因为除了知识管理相关的内容之外&#xff0c;还有知识产权相…

Matlab 最小二乘法拟合平面(SVD)

文章目录 一、简介1.1最小二乘法拟合平面1.2 SVD角度二、实现代码三、实现效果参考资料一、简介 1.1最小二乘法拟合平面 之前我们使用过最为经典的方式对平面进行了最小二乘拟合(点云最小二乘法拟合平面),其推导过程如下所示: 仔细观察一下可以发现

IP协议

网络层的一个重要作用就是把世界上的地址能够以一定的规范定义出来。地址管理路由选择网络层的代表:IP协议4位版本指的是&#xff1a;此处的取值只有两个ipv4,ipv64位首部长度&#xff1a;描述了ip报头有多长&#xff08;ip报头是变长的&#xff09;报头中有一个选项部分&#…

JUnit5文档整理

1、Overview 1.1、Junit5是什么 与以前的JUnit版本不同&#xff0c;JUnit 5是由三个不同子项目的几个不同的模块组成。 JUnit 5 JUnit Platform&#xff08;基础平台&#xff09; JUnit Jupiter&#xff08;核心程序&#xff09; JUnit Vintage&#xff08;老版本的支持&a…

JVM那些事——垃圾回收和内存分配

内存分配 默认情况下新生代和老年区的内存比例是1:2&#xff0c;新生代中Eden区和Survivor区的比例是8:1。 对象优先分配在Eden区。大对象直接进入老年区。通过-XX:PertenureizeThreshold参数设置临界值。长期存活的对象进入老年区。对象每熬过一次Minor GC&#xff0c;年龄1&…

【面试题】Map和Set

1. Map和Object的区别 形式不同 // Object var obj {key1: hello,key2: 100,key3: {x: 100} } // Map var m new Map([[key1, hello],[key2, 100],[key3, {x: 100}] ])API不同 // Map的API m.set(name, 小明) // 新增 m.delete(key2) // 删除 m.has(key3) // …