[SpringBoot] APIインターフェースがブラッシングされる(頻繁にアクセスされる)のを防ぎ、コード実行SQLスクリプト

序文

最近、ステーションbでvueの実際の戦闘ビデオを視聴しました。このビデオでは、vueの使用を要求するためにサーバーのAPIインターフェイスを構築する必要があります。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が頻繁にアクセスされないようにする

カスタムアノテーション:遅延

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