关于Spring中拦截器的使用

因最近看接口幂等性解决方案,以及接口重复提交的问题,所以记录一下Spring的拦截器的使用.

1 拦截器的概述

1 拦截器说明及应用场景

拦截器: 本身会在请求进入Controller控制层前后做拦截处理.

常见使用场景:

  • 接口重复提交校验 放置用户重复提交相同数据.

  • 权限检查 当前用户是否登录,是否有权限访问数据.

  • 日志记录 记录请求信息,输出成日志文件.

  • 性能监控 慢日志.

2 Spring中的拦截器 HandlerInterceptor
public interface HandlerInterceptor {

	/**
     前处理回调方法,第三个参数为响应的处理器,自定义Controller
     返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断,不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
	 */
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	/**
	后处理回调方法,实现处理器的后处理(在渲染视图之前),可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
	 */
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	/**
	整个请求处理完毕回调方法,即在视图渲染完毕时回调,在性能监控中可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,但仅调用处理器执行链中.
	 */
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
3 流程说明

1 拦截器按照执行顺序执行,而执行顺序是按照Spring配置文件中定义的顺序执行.在实现WebMvcConfigurer接口的配置类中,按照注册顺序执行,即先注册的拦截器先执行.

2 拦截器正常执行流程图

​ 每个拦截器都返回True,则继续向下一个拦截器执行,最后进入控制层.

HandlerInterceptor1.preHandle
HandlerInterceptor2.preHandle
Controller控制层
HandlerInterceptor2.postHandle
HandlerInterceptor1.postHandle
View渲染
HandlerInterceptor2.afterCompletion
HandlerInterceptor1.afterCompletion

3 拦截器异常执行流程图

​ 拦截器2返回false,即不满足拦截器设置的规则,直接返回.

HandlerInterceptor1.preHandle
HandlerInterceptor2.preHandle
Controller控制层
HandlerInterceptor2.postHandle
HandlerInterceptor1.postHandle
View渲染
HandlerInterceptor2.afterCompletion
HandlerInterceptor1.afterCompletion
返回false,回到拦截器1

2 简单使用案例

1 准备环境

搭建一个可以运行的SpringBoot环境.

1 准备文件

1 application.yml
server:
  port: 8081
spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/test
2 实体类
@Data
public class User {

    private String id;

}
3 Controller控制器
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {

    @RequestMapping("/query")
    public String queryById() {
        User user = new User();
        log.info("请求参数=={}", user.toString());
        log.info("响应参数=={}", user.toString());
        return "<h1>" + user.toString() + "<h1>";
    }

}
4 拦截器1
@Component
@Slf4j
public class MyInterceptor implements HandlerInterceptor {

    /**
     * 请求处理前调用
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("========================进入拦截器1=================================");
        log.info("请求request = {}",request.toString());
        log.info("请求response = {}",response.toString());
        log.info("请求handler = {}",handler.toString());

        return true;
    }

    /**
     * 请求处理后,渲染ModelAndView前调用
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("=============进入拦截器1,请求处理后,渲染ModelAndView前调用。=================================");
    }

    /**
     * 渲染ModelAndView后调用
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("=============进入拦截器1,渲染ModelAndView后调用。=================================");
    }
5 拦截器2
@Component
@Slf4j
public class MyInterceptor2 implements HandlerInterceptor {

    /**
     * 请求处理前调用
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("========================进入拦截器2=================================");
        log.info("请求request = {}",request.toString());
        log.info("请求response = {}",response.toString());
        log.info("请求handler = {}",handler.toString());

        return true;
    }

    /**
     * 请求处理后,渲染ModelAndView前调用
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("=============进入拦截器2,请求处理后,渲染ModelAndView前调用。=================================");
    }

    /**
     * 渲染ModelAndView后调用
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("=============进入拦截器2,渲染ModelAndView后调用。=================================");
    }
}
6 配置类
@Configuration
public class MyConfigurer implements WebMvcConfigurer {

    @Autowired
    private MyInterceptor myInterceptor;

    @Autowired
    private MyInterceptor2 myInterceptor2;

    /**
     * 用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
     * 注册的先后顺序就是拦截器的执行顺序,先注册,先执行
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 列如
        // addPathPatterns("/**") 表示拦截所有的请求
        // excludePathPatterns("/login", "/register") 表示除了登陆与注册之外
        registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/login", "/register");
        registry.addInterceptor(myInterceptor2).addPathPatterns("/**").excludePathPatterns("/login", "/register");
    }

    /**
     * 用来配置静态资源的,比如html,js,css,等等,列如在使用swagger文档时
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

    }
}

3 效果验证

1 浏览器地址栏发送请求

http://localhost:8081/consumer/query

2 页面结果

image-20210814211012017

3 日志结果

image-20210814211116611

image-20210814211149083

3 使用场景及解决方案

关于防重复提交或接口恶意多次请求

方法1 前台处理

前端: 按钮置灰不可选

方法2 通过一个唯一字符串

前端进入提交页面时,会调用后台,获取一个唯一的字符串返回,字符串在后台可保存有效时间,有效时间外,提交无效.待表单提交后,删除该唯一字符串.

方法3 后台通过注解+拦截器

1 准备环境,和上述一样
2 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

    /**
     * 保存时间,默认8秒
     *
     * @return
     */
    int seconds() default 8;

    /**
     * 最大方式次数,默认1次
     *
     * @return
     */
    int maxCount() default 1;


}
3 定义拦截器
@Component
@Slf4j
public class MyInterceptor implements HandlerInterceptor {

    // 缓存方案
    // 方案1 使用redis保存该ip在一定时间内调用接口的次数 (适用集群)
//    @Autowired
//    private RedisTemplate redisTemplate;

    public static final int IntervalTime = 8;

    // 方案2 谷歌的guava , 使用本地缓存,设置有效期8秒 (适用单体)
    private final Cache<String, Integer> cache = CacheBuilder.newBuilder()
            .expireAfterAccess(IntervalTime, TimeUnit.SECONDS).build();


    /**
     * 请求处理前调用
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("========================进入拦截器1=================================");

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        // 查看该方法上是否有需要拦截的注释
        MyAnnotation myAnnotation = handlerMethod.getMethodAnnotation(MyAnnotation.class);
        // 注解存在
        if (myAnnotation != null) {
            int maxCount = myAnnotation.maxCount();
            int seconds = myAnnotation.seconds();

            // 1 获取ip  ip可能代理
            // 代理服务器在请求转发时添加上去的
            String ip = request.getHeader("x-forwarded-for");
            log.info("x-forwarded-for = {} ", ip);

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
                log.info("Proxy-Client-IP = {} ", ip);
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
                log.info("WL-Proxy-Client-IP = {} ", ip);
            }
            // remote_addr http协议传输的时候自动添加,不受请求头header的控制
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
                log.info("remote_addr = {} ", ip);
            }
            // 缓存方案1 redis
//            if (redisCache(response, maxCount, seconds, ip)) {return false;}

            // 缓存方案2 Google的guava
            if (googleCache(response, maxCount, seconds, ip)) {return false;}
        }

        // 不拦截,进入下一个拦截器
        return true;
    }

    /**
     * 使用Google的guava
     * @param response
     * @param maxCount
     * @param seconds
     * @param ip
     * @return
     * @throws IOException
     */
    private boolean googleCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException, ExecutionException {
        // 2 从缓存中获取该ip的访问次数
        Integer count = cache.getIfPresent(ip);

        // 3 判断是否满足注解设置要求
        if (count == null) {
            cache.put(ip,1);
        } else if (count < maxCount) {
            // 3.2 在有效期内,访问次数满足要求
            cache.put(ip,++count);
        } else {
            // 3.3 超过最大访问次数,拒绝该请求
            log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            ResultResponse resultResponse = new ResultResponse();
            resultResponse.setResult("操作太快,请稍后访问系统");
            Object obj = JSONObject.toJSON(resultResponse);
            response.getWriter().write(JSONObject.toJSONString(obj));
            return true;
        }
        return false;
    }


    /**
     *  使用redis作为缓存
     * @param response
     * @param maxCount
     * @param seconds
     * @param ip
     * @return
     * @throws IOException
     */
/*    private boolean redisCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException {
        // 2 从redis中获取该ip的访问次数
        Integer count = (Integer) redisTemplate.opsForValue().get(ip);

        // 3 判断是否满足注解设置要求
        if (count == null) {
            // 3.1 第一次访问,设置次数,有效期
            redisTemplate.opsForValue().set(ip, 1);
            redisTemplate.expire(ip, seconds, TimeUnit.SECONDS);
        } else if (count < maxCount) {
            // 3.2 在有效期内,访问次数满足要求
            redisTemplate.opsForValue().set(ip, ++count);
        } else {
            // 3.3 超过最大访问次数,拒绝该请求

            log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);

            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");

            ResultResponse resultResponse = new ResultResponse();
            resultResponse.setResult("操作太快,请稍后访问系统");
            Object obj = JSONObject.toJSON(resultResponse);
            response.getWriter().write(JSONObject.toJSONString(obj));
            return true;
        }
        return false;
    }*/

    /**
     * 请求处理后,渲染ModelAndView前调用
     *
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("=============进入拦截器1,请求处理后,渲染ModelAndView前调用。=================================");
    }

    /**
     * 渲染ModelAndView后调用
     *
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("=============进入拦截器1,渲染ModelAndView后调用。=================================");
    }
}
4 控制层添加自定义注解
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {

    @Autowired
    private ConsumerService consumerService;

    @MyAnnotation()
    @RequestMapping("/query")
    public String queryById() {
        User user = new User();
        log.info("请求参数=={}", user.toString());
        log.info("响应参数=={}", user.toString());
        return "<h1>" + user.toString() + "<h1>";
    }

}
5 效果验证

1 在浏览器地址栏访问

http://localhost:8081/consumer/query

2 初次访问

image-20210814224542372

3 在规定时间内多次访问

image-20210814224606879

4 控制台日志信息

image-20210814224702378

4 总结

关于Spring的拦截器,在平时的项目中,使用较多,使用的场景也很丰富,配合自定义注解,可以很好的实现一个开关效果.后续有其他场景,一一记录.

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值