Spring Security技术栈开发企业级认证与授权(五)使用Filter、Interceptor和AOP拦截REST服务

2020-04-03 17:09:22 浏览数 (1)

一般情况,在访问RESTful风格的API之前,可以对访问行为进行拦截,并做一些逻辑处理,本文主要介绍三种拦截方式,分别是:过滤器Filter、拦截器Interceptor以及面向切面的拦截方式AOP

一、使用过滤器Filter进行拦截

使用过滤器进行拦截主要有两种方式,第一种是将自定义的拦截器标注为SpringBean,在Spring Boot应用就可以对RESTful风格的API进行拦截。第二种方式往往应用在继承第三方过滤器,这时候就需要将第三方拦截器使用FilterRegistrationBean对象进行注册即可。接下来详细介绍两种方式。

  • 将拦截器标注为SpringBean
代码语言:javascript复制
package com.lemon.security.web.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

/**
 * @author lemon
 * @date 2018/4/1 下午10:19
 */
@Component
public class TimeFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("time filter init.");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("time filter start.");
        long startTime = System.currentTimeMillis();
        chain.doFilter(request, response);
        System.out.println("time filter 耗时: "   (System.currentTimeMillis() - startTime));
        System.out.println("time filter finish.");
    }

    @Override
    public void destroy() {
        System.out.println("time filter destroy.");
    }
}

启动Spring Boot应用的时候,上面的拦截器就会起作用,当访问每一个服务的时候,都会进入这个拦截器中。初始化方法init和销毁方法destroy只会调用一次,分别是应用启动时候调用init方法,应用关闭时候调用destroy方法。而doFilter方法则在每次都会调用。

  • 将拦截器作为第三方拦截器进行注册

使用的类还是上面的同一个类,只不过这次不需要@Component注解,这时候我们需要自己写一个配置类,将过滤器注册到Spring容器中。推荐使用这种方式,因为这种方式我们可以自己设置需要拦截的API,否则第一种方式是拦截所有的API

代码语言:javascript复制
package com.lemon.security.web.config;

import com.lemon.security.web.filter.TimeFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * @author lemon
 * @date 2018/4/1 下午10:34
 */
@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean timeFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        TimeFilter timeFilter = new TimeFilter();
        filterRegistrationBean.setFilter(timeFilter);
        List<String> urls = new ArrayList<>();
        urls.add("/*");
        filterRegistrationBean.setUrlPatterns(urls);
        return filterRegistrationBean;
    }
}

这里我设置的仍然是拦截所有的API,可以设置为自定义的方式对API进行拦截。

二、使用拦截器Interceptor进行拦截

这里需要定义一个拦截器类,并实现HandlerInterceptor接口,这个接口有三个方法需要实现,分别是:

代码语言:javascript复制
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception;
void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;
void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;

下面对三个方法进行一一解释:

  • preHandle方法的第三个参数是具体的API处理方法的Method对象,我们可以将其强转为HandlerMethod,然后就可以获取该Method的一些属性,比如方法名,方法所在类的类名等信息。preHandle是当访问API之前,都要进入这个方法,由这个方法进行一些逻辑处理,如果处理完结果返回true,那么将继续进入到具体的API中,否则将就地结束访问,逻辑不会进入API方法中。
  • postHandle方法是在API方法访问完成之后立即进入的方法,可以处理一些逻辑,比如将API中的数据封装到ModelAndView中,如果前面的preHandle方法返回false,将不会执行该方法,如果API方法发生了异常,也将不会调用此方法。
  • afterCompletion方法的调用只要preHandle方法通过之后就会调用它,不论API方法是否出现了异常。如果出现了异常,将被封装到Exception对象中。

下面,写一个自定义的类来实现上述接口:

代码语言:javascript复制
package com.lemon.security.web.interceptor;

import org.springframework.stereotype.Component;
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;

/**
 * @author lemon
 * @date 2018/4/1 下午10:39
 */
@Component
public class TimeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandler");
        System.out.println(((HandlerMethod) handler).getBean().getClass().getName());
        System.out.println(((HandlerMethod) handler).getMethod().getName());
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandler");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
        System.out.println("TimeInterceptor耗时:"   (System.currentTimeMillis() - (Long) request.getAttribute("startTime")));
    }
}

这里需要将其标注为SpringBean,但是仅仅标注为Bean还是不够的,需要在配置类中进行配置。代码如下:

代码语言:javascript复制
package com.lemon.security.web.config;

import com.lemon.security.web.interceptor.TimeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * @author lemon
 * @date 2018/4/1 下午10:34
 */
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private TimeInterceptor timeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(timeInterceptor);
    }
}

这个配置类需要继承WebMvcConfigurerAdapter,并重写添加拦截器的方法addInterceptors,将自定义拦截器添加到应用中。这时候拦截器就生效了。

三、使用AOP进行拦截

其实是有拦截器InterceptorAPI进行拦截的时候是有缺陷的,因为无法获取前端访问API的时候所携带的参数的,为什么会这么说?从Spring MVCDispatcherServlet的源代码中可以发现,找到doDispatch方法,也就是请求分发的方法,有一段代码如下:

如果我们自定的InterceptorpreHandler方法返回的是false,分发任务就会截止,不再继续执行下面的代码,而下面的一行代码正是将前端携带的参数进行映射的逻辑,也就是说,preHandler方法不会接触到前端携带来的参数,也就是说拦截器无法处理参数。所以这里引进AOP进行拦截。 AOP的核心概念解释: 描述AOP常用的一些术语有通知(Adivce)、切点(Pointcut)、连接点(Join point)、切面(Aspect)、引入(Introduction)、织入(Weaving

  • 通知(Advice

通知分为五中类型: Before:在方法被调用之前调用 After:在方法完成后调用通知,无论方法是否执行成功 After-returning:在方法成功执行之后调用通知 After-throwing:在方法抛出异常后调用通知 Around:通知了好、包含了被通知的方法,在被通知的方法调用之前后调用之后执行自定义的行为

  • 连接点(Join point

连接点是一个应用执行过程中能够插入一个切面的点。比如:方法调用、方法执行、字段设置/获取、异常处理执行、类初始化、甚至是for循环中的某个点。理论上, 程序执行过程中的任何时点都可以作为作为织入点, 而所有这些执行时点都是Joint point,但 Spring AOP 目前仅支持方法执行 (method execution)。

  • 切点(Pointcut

通知(advice)定义了切面何时,那么切点就是定义切面“何处” 描述某一类 Joint points, 比如定义了很多 Joint point, 对于 Spring AOP 来说就是匹配哪些方法的执行。

  • 切面(Aspect

切面是切点和通知的结合。通知和切点共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能。

  • 引入(Introduction

引用允许我们向现有的类添加新的方法或者属性

  • 织入(Weaving

组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

上面的概念有点生涩难懂,总结一个核心内容:切面 = 切点 通知。 现在通过代码来编写一个切面:

代码语言:javascript复制
package com.lemon.security.web.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author lemon
 * @date 2018/4/2 上午10:40
 */
@Aspect
@Component
public class TimeAspect {

    @Around("execution(* com.lemon.security.web.controller.UserController.*(..))")
    public Object handleTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("time aspect is start.");
        for (Object object : proceedingJoinPoint.getArgs()) {
            System.out.println(object);
        }
        long startTime = System.currentTimeMillis();
        Object obj = proceedingJoinPoint.proceed();
        System.out.println("time aspect 耗时:"   (System.currentTimeMillis() - startTime));
        System.out.println("time aspect finish.");
        return obj;
    }
}

@Around定义了环绕通知,也就是定义了何时使用切面,表达式"execution(* com.lemon.security.web.controller.UserController.*(..))"定义了再哪里使用。ProceedingJoinPoint对象的proceed()方法表示执行被拦截的方法,它有一个Object类型的返回值,是原有方法的返回值,后期使用的时候往往需要强转。关于切点的表达式,可以访问Spring官方文档。 对于上面三种拦截方式,他们的执行有一个基本的顺序,进入的顺序是Filter-->Interceptor-->Aspect-->Controller-->Aspect-->Interceptor-->Filter(不考虑异常的发生)。如下图所示:

0 人点赞