Spring Security技术栈学习笔记(五)使用Filter、Interceptor和AOP拦截REST服务

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

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

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

  • 将拦截器标注为SpringBean
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


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接口,这个接口有三个方法需要实现,分别是:

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对象中。

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

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还是不够的,需要在配置类中进行配置。代码如下:

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框架一样,在运行时完成织入。

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

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(不考虑异常的发生)。如下图所示:
这里写图片描述

Spring Security技术栈开发企业级认证与授权系列文章列表:

Spring Security技术栈学习笔记(一)环境搭建
Spring Security技术栈学习笔记(二)RESTful API详解
Spring Security技术栈学习笔记(三)表单校验以及自定义校验注解开发
Spring Security技术栈学习笔记(四)RESTful API服务异常处理
Spring Security技术栈学习笔记(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈学习笔记(六)使用REST方式处理文件服务
Spring Security技术栈学习笔记(七)使用Swagger自动生成API文档
Spring Security技术栈学习笔记(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈学习笔记(九)开发图形验证码接口
Spring Security技术栈学习笔记(十)开发记住我功能
Spring Security技术栈学习笔记(十一)开发短信验证码登录
Spring Security技术栈学习笔记(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈学习笔记(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈学习笔记(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈学习笔记(十六)使用Spring Social集成微信登录验证方式

示例代码下载地址:

项目已经上传到码云,欢迎下载,内容所在文件夹为chapter005

更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)
在这里插入图片描述

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值