【SpringBoot】防止API接口被刷(频繁访问),以及代码执行sql脚本

前言

最近在b站看vue的实战视频,其中需要搭建服务端的api接口,以给vue请求使用。学习资料里面提供了api的项目代码。在本地运行,每次都要启动,有点麻烦。使用老师搭建的api,由于是公用的,数据库数据经常被其他学习的同学删除或修改。

为了一劳永逸,也为了各位小伙伴一起愉快学习。我就用SpringBoot写了一个小程序,放到服务器运行。每天凌晨2点定时执行sql脚本,将数据库数据重新导入。同时为了紧急情况,提供了一个公开访问的api接口,随时可以通过访问该api,将数据库的数据重置。这样,如果一起使用相同的服务端,再也不怕数据的被其他小伙伴改掉了。随时就可以自己重置。

但是重置数据库的操作,是完整地执行整个sql脚本,完整执行需要差不多1分钟的时间,极其占用数据库和服务器资源。如果重置数据库的接口直接暴露,可能会被其他人恶意访问(狂刷)。那么我的服务器会崩的,所以必需给重置数据库的接口限制防刷。其实也比较简单,不多说了,直接看代码。

重置数据库数据(执行sql脚本)

DataController

package net.ysq.vueshopdata.controller;

import net.ysq.vueshopdata.component.Delay;
import net.ysq.vueshopdata.service.DataService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.sql.SQLException;

/**
 * @author passerbyYSQ
 * @create 2020-08-27 23:22
 */
@Controller
@ResponseBody
@EnableScheduling   // 2.开启定时任务
@RequestMapping("/vueshop")
public class DataController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DataService dataService;

    @RequestMapping("log")
    public void testLog() {
        logger.info("测试log");
    }

    @Scheduled(cron = "0 0 2 * * ?") // 定时任务,每天凌晨2点,执行该方法
    @Delay(time = 1000 * 60) // 使用自定义注解。接口被成功访问后的1分钟之内,其他请求均拦截并驳回
    @RequestMapping("/reset")
    public String resetDataBase() throws SQLException {
        logger.info("【开始】重置数据库vue_shop的数据");

        long start = System.currentTimeMillis();
        dataService.resetDataBase("mydb.sql");
        long end = System.currentTimeMillis();

        String cost = "用时:" + (end - start) / 1000.0 + "秒";
        logger.info("【结束】重置成功:" + cost);

        return cost;
    }

}

DataService

package net.ysq.vueshopdata.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * @author passerbyYSQ
 * @create 2020-08-28 0:16
 */
@Service // 不要忘了加入IOC容器,否则无法使用@Autowired注入
public class DataService {

    @Autowired
    private DataSource dataSource;

    /**
     * 重置数据库数据
     * @param sqlScriptName 需要放在resources文件夹下面
     */
    public boolean resetDataBase(String sqlScriptName) {
        Connection conn = null;
        try {
            // 从Druid数据源(数据库连接池)中获取一个数据库连接
            conn = dataSource.getConnection();
            ClassPathResource rc = new ClassPathResource(sqlScriptName);
            EncodedResource er = new EncodedResource(rc, StandardCharsets.UTF_8);
            // ScriptUtils:是SpringBoot源码中使用的工具类,能够执行Sql脚本
            // sql脚本执行中途,遇到错误,默认会抛出异常,停止执行
            // 建议传入参数true,忽略中途的错误,但是后面4个参数又是必需的,只需要填入源码中的默认值即可
            ScriptUtils.executeSqlScript(conn, er, true, true,
                    "--", ";", "/*", "*/");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 不要忘了释放连接
            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

防止API被频繁访问

自定义注解:Delay

package net.ysq.vueshopdata.component;

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 该注解放在controller层的方法上(api),
 * 用于防止别人恶意访问(刷)一些耗时占资源的接口
 * @author passerbyYSQ
 * @create 2020-08-28 18:25
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Delay {
    // 默认两秒,表示:当api被成功访问后的2秒内,其他访问请求均拦截并驳回
    int time() default 2000;

}

自定义拦截器:RequestFrequencyInterceptor

package net.ysq.vueshopdata.component;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @author passerbyYSQ
 * @create 2020-08-28 18:26
 */
@Component // 加入IOC容器中,以用于在配置类中注册拦截器
public class RequestFrequencyInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 由于该注解可能加到多个方法(接口)上,每个接口上一次的访问时间都不一样。
     * key值:必须能够唯一标识方法(接口),由于不同的类中可能会出现同名的方法,所以
     * 并不建议直接使用方法名作为key值,
     * value值:接口上一次被成功访问(驳回的访问不算)的时间
     */
    private Map<String, Long> lastTimeMap = new HashMap<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if(handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 作为标识该接口的key值
            String methodKey = handlerMethod.getBean().getClass().getName();
            // 如果该方法(接口)上有Delay注解,则获取
            Delay delay = handlerMethod.getMethodAnnotation(Delay.class);

            if (delay != null) {

                Long currentTime = System.currentTimeMillis();

                if (!lastTimeMap.containsKey(methodKey)) {
                    // 接口被第一次访问,没有lastTime
                    lastTimeMap.put(methodKey, currentTime);
                    // 放行请求
                    return true;
                } else {
                    // 方法正在被频繁访问,访问间隔小于delay.time(),请稍后重试
                    if (currentTime - lastTimeMap.get(methodKey) < delay.time()) {

                        logger.info("【频繁访问】,已被拦截");

                        String responseMsg = String.format("接口正在被频繁访问,访问间隔小于%d秒。您已被拦截,请稍后重试", delay.time() / 1000);
                        response.setContentType("application/json;charset=utf-8"); // 不设置中文会出现乱码
                        response.getWriter().write(responseMsg);
                        // 拦截
                        return false;
                    } else {
                        // 大于间隔,更新接口上一次被成功访问的时间,并放行请求
                        lastTimeMap.put(methodKey, currentTime);
                        return true;
                    }
                }
            }
        }

        // 该拦截器只处理访问被Delay注解标识的接口的请求,访问其他接口的请求不作拦截,一律放行
        return true;
    }
}

WebMvc的配置类:WebMvcConfig,注册上面的拦截器

package net.ysq.vueshopdata.config;

import net.ysq.vueshopdata.component.RequestFrequencyInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author passerbyYSQ
 * @create 2020-08-28 18:45
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RequestFrequencyInterceptor frequencyInterceptor;

    /**
     * 注册拦截器RequestFrequencyInterceptor
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(frequencyInterceptor)
                .addPathPatterns( "/**" ).excludePathPatterns("/error");
    }

    /**
     * 支持跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH")
                .allowCredentials(true).maxAge(3600);
    }
}

猜你喜欢

转载自blog.csdn.net/qq_43290318/article/details/108289754