拦截器应用

概要

记录一下项目里用到的拦截器

拦截器应用

1.在访问需要token的页面时,在拦截器会对token做校验
2.做限流(10s只能生成两次短链,一分钟内验证码只能发送一次)

拦截器

实现WebMvcConfigurer接口

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AccessLimitInterceptor accessLimitInterceptor;

    @Autowired
    private TraceIdInterceptor traceIdInterceptor;

    @Autowired
    private TenantAuthenticeInterceptor tenantAuthenticeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 配置需要拦截上下文列表(除了配置的这些,剩下的都不拦)
        List<String> tenantIncludePaths = new ArrayList<>();
        
        tenantIncludePaths.add("/tenant/url/**"); // 租户url短链管理
        tenantIncludePaths.add("/tenant/log/**"); // 租户短链日志管理
        tenantIncludePaths.add("/tenant/dashboard/**"); // 租户仪表盘
        tenantIncludePaths.add("/tenant/statistic/**"); // 租户数据统计管理

        // 注册租户会话拦截器
        registry.addInterceptor(tenantAuthenticeInterceptor)
                .addPathPatterns(tenantIncludePaths);

        // 注册限流拦截器
        registry.addInterceptor(accessLimitInterceptor)
                .addPathPatterns("/**");

}

登录

登录成功后,将token存到redis和cookie

// 数据写入redis,登录成功
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        redisTemplate.opsForValue().set(SystemConst.SYSTEM_TENANT_KEY + ":" + token, JsonUtils.writeValueAsString(tenantInfo), 1800, TimeUnit.SECONDS);

        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put(SystemConst.SYSTEM_TENANT_TOKEN, token);
        // 将token放在cookie中
        CookieUtils.setCookie(response, SystemConst.SYSTEM_TENANT_TOKEN, token);

拦截器先判断请求方式,再判断token是否为空-》是否存在-》value是否合法(key【token】-value【用户信息】),合法刷新缓存时长,不合法跳回登录页

@Component
public class TenantAuthenticeInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /*
     * 进入controller层之前拦截请求
     * 返回值:表示是否将当前的请求拦截下来  false:拦截请求,请求别终止。true:请求不被拦截,继续执行
     * Object obj:表示被拦的请求的目标对象(controller中方法)
     */

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        // 判断请求类型,如果是OPTIONS,直接返回
        String options = HttpMethod.OPTIONS.toString();
        if (options.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // 先判断token是否为空
        String token = TenantAuthenticeUtil.getToken(request);
        if (logger.isInfoEnabled()) {
            logger.info("TenantAuthenticeInterceptor -- preHandle -- token = {}", token);
        }
        if (StringUtils.isBlank(token)) {
            responseError(request, response);
            return false;
        }

        // 再判断token是否存在
        String tenantInfoString = redisTemplate.opsForValue().get(SystemConst.SYSTEM_TENANT_KEY + ":" + token);
        //判断获取到的字符串是否为空或者字符串为""
        if (StringUtils.isBlank(tenantInfoString)) {
            responseError(request, response);
            return false;
        }

        // 再判断token是否合法
        //将上述获取到的字符串转成Map的格式
        Map<String, Object> tenantInfo = (Map<String,Object>) JsonUtils.readValue(tenantInfoString, Map.class);
        if (logger.isInfoEnabled()) {
            logger.info("TenantAuthenticeInterceptor -- preHandle -- tenantInfo = {}", tenantInfo);
        }
        //判断Map是否是空或者""
        if (tenantInfo == null || tenantInfo.isEmpty()) {
            responseError(request, response);
            return false;
        }
        //可以直接将字符串转成Map格式而不用先判断字符串是否为空,但是先判断字符串是否空的代价更小

        // 刷新会话缓存时长
        redisTemplate.expire(SystemConst.SYSTEM_TENANT_KEY + ":" + token, 1800, TimeUnit.SECONDS);

        TenantHolder.setTenant(tenantInfo);

        // 合格不需要拦截,放行
        return true;
    }

    /*
     * 处理请求完成后视图渲染之前的处理操作
     * 通过ModelAndView参数改变显示的视图,或发往视图的方法
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        //logger.info("TenantAuthenticeInterceptor -- postHandle -- 执行了");
    }

    /*
     * 视图渲染之后的操作
     */
    @Override
    public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception {
        //logger.info("TenantAuthenticeInterceptor -- afterCompletion -- 执行了");
        TenantHolder.clearTenant();
    }


    /**
     * 无权限时的返回
     *
     * @param request
     * @param response
     * @throws IOException
     */
    private void responseError(HttpServletRequest request, HttpServletResponse response) throws IOException {
            // 拦截后跳转至登录页
            response.sendRedirect("/tenant/login");
    }

}

1.options请求我理解的是出现了跨域,需要先确认是否支持跨域(即发送options请求),确认支持以后再发送正式请求(GET、POST...)

2.在判断token是否为空之后,会到redis里面判断token是否存在,存在后是先判断获取到的字符串是否为空,再对字符串转Map格式进一步判断Map里面是否为空,而不是直接转成Map格式再判断Map里面是否为空。先做前置判断的话代价要更小

限流

这里使用了注解做限流标注

/**
 * @description seconds 秒内只能执行 maxCount 次
 * 自定义注解包含的常见的三个注解
 **/
@Target(ElementType.METHOD) 
@Retention(RetentionPolicy.RUNTIME)    
@Documented    
public @interface AccessLimit {

    /**
     * 限制周期(秒)
     */
    int seconds();

    /**
     * 规定周期内最大次数
     */
    int maxCount();

    /**
     * 触发限制时的消息提示
     */
    String msg() default "操作频率过高,请稍后再试!";
}

自定义注解的使用
在这里插入图片描述

在这里插入图片描述

限流拦截器

@Component
public class AccessLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            // 判断请求类型,如果是OPTIONS,直接返回(OPTIONS是预检,判断是否支持跨域,支持才会正式发送请求)
            String options = HttpMethod.OPTIONS.toString();
            if (options.equals(request.getMethod())) {
                response.setStatus(HttpServletResponse.SC_OK);
                return true;
            }

            HandlerMethod handlerMethod = (HandlerMethod) handler;
            AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
            // 接口上没有注解,说明这个接口不做限制
            if (accessLimit == null) {
                return true;
            }

            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();
            String ip = IpAddressUtils.getIpAddress(request);
            String method = request.getMethod();
            String requestURI = request.getRequestURI().replace("/", "");

            //这里URI可以用URL代替(这里需要加的原因是这个注解好几个地方用到了(门户生成短链和邮箱验证码生成),都是同一个ip和post,但又不能相互影响,所以加URI)
            String redisKey = ip + ":" + method + ":" + requestURI;
            Object redisResult = redisTemplate.opsForValue().get(redisKey);
            // 获取当前访问次数
            Integer count = JsonUtils.convertValue(redisResult, Integer.class);
            if (count == null) {
                // 在规定周期内第一次访问,存入redis,次数+1
                redisTemplate.opsForValue().increment(redisKey, 1);
                redisTemplate.expire(redisKey, seconds, TimeUnit.SECONDS);
            } else {
                if (count >= maxCount) {
                    // 超出访问限制次数
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    Result result = Result.create(403, accessLimit.msg());
                    out.write(JsonUtils.writeValueAsString(result));
                    out.flush();
                    out.close();
                    return false;
                } else {
                    // 没超出访问限制次数,则继续次数+1
                    redisTemplate.opsForValue().increment(redisKey, 1);
                }
            }
        }
        return true;
    }
}

小结

拦截器我觉得是AOP的一种应用,在请求调用前后会经过拦截器
拦截器的处理顺序是按照注册的先后顺序来
在这里插入图片描述

preHandle():处理方法前
postHandle():处理方法后,视图渲染前
afterCompletion():视图渲染后

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值