###### # 简介

由于前端会莫名其妙的对同一接口请求多次，从而占用后端资源造成浪费。所以采用了后端拦截相关重复请求的方案。此方案会将请求用户 id 加接口 url 加参数作为 key，请求时间作为 value，使用 ConcurrentHashMap 进行缓存。如果下次相同的请求和上次请求的时间在指定的范围内则认为此请求属于重复请求。

###### # 自定义可重复读 Request

request 的 body 只能读取一次，所以对其进行封装。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120

    package xxx.support;

    import com.alibaba.fastjson.JSON;
    import lombok.extern.slf4j.Slf4j;

    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.*;
    import java.nio.charset.StandardCharsets;
    import java.util.Enumeration;
    import java.util.Map;
    import java.util.Objects;
    import java.util.TreeMap;

    @Slf4j
    public class RepeatableReadHttpServletRequestWrapper extends HttpServletRequestWrapper {

        private final byte[] requestBody;

        public RepeatableReadHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            this.requestBody = readRequestBody(request);
        }

        private byte[] readRequestBody(HttpServletRequest request) throws IOException {
            try (InputStream inputStream = request.getInputStream();
                 ByteArrayOutputStream result = new ByteArrayOutputStream()) {

                byte[] buffer = new byte[1024];
                int length;
                while ((length = inputStream.read(buffer)) != -1) {
                    result.write(buffer, 0, length);
                }

                return result.toByteArray();
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            // 直接使用 ByteArrayInputStream，它提供可重复读取的输入流
            return new ServletInputStream() {
                private final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);

                @Override
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }

                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.available() == 0;
                }

                @Override
                public boolean isReady() {
                    return true;
                }

                @Override
                public void setReadListener(ReadListener readListener) {
                    // 不需要实现，可以留空
                }
            };
        }

        @Override
        public BufferedReader getReader() throws IOException {
            // 使用 InputStreamReader 包装 ByteArrayInputStream，提供可重复读取的字符流
            return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(requestBody)));
        }

        /**
         * 获取json格式的参数
         * @return
         */
        public String getParamsToJSONString() {
            String jsonStr = "";
            if ("POST".equals(this.getMethod().toUpperCase()) && this.isJsonRequest()) {
                try {
                    jsonStr = this.readJsonData();
                } catch (Exception e) {
                    log.error(e.getMessage());
                }
            } else {
                Enumeration<String> parameterNames = this.getParameterNames();
                if (Objects.nonNull(parameterNames) && parameterNames.hasMoreElements()) {
                    // 将参数排序后转为json
                    Map<String, String> paramsMap = new TreeMap<>();
                    while (parameterNames.hasMoreElements()) {
                        String paramName = parameterNames.nextElement();
                        paramsMap.put(paramName, this.getParameter(paramName));
                    }
                    jsonStr = JSON.toJSONString(paramsMap);
                }
            }
            return jsonStr;
        }

        /**
         * 判断是否json请求
         * @return
         */
        private boolean isJsonRequest() {
            String contentType = this.getContentType();
            return contentType != null && contentType.toLowerCase().contains("application/json");
        }

        /**
         * 获取json格式的参数
         * @return
         * @throws IOException
         */
        private String readJsonData() throws IOException {
            return new String(this.readRequestBody(this), StandardCharsets.UTF_8);
        }

    }

###### # 重复请求过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106

    package xxx.filter;

    import cn.hutool.core.collection.CollectionUtil;
    import xxx.RepeatableReadHttpServletRequestWrapper;
    import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
    import org.springframework.security.web.util.matcher.RequestMatcher;
    import org.springframework.web.filter.OncePerRequestFilter;

    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.concurrent.ConcurrentHashMap;

    public class DuplicateRequestFilter extends OncePerRequestFilter {
        // 是否启用
        private Boolean duplicateRequestFilter;
        // 间隔时间（毫秒）
        private Long intervalTime;
        // 清除缓存时间（毫秒）
        private Long clearCachetime;
        // 放行url
        private List<RequestMatcher> permitAll;

        public DuplicateRequestFilter(Boolean duplicateRequestFilter, List<RequestMatcher> permitAll, Long intervalTime,
                                      Long clearCachetime) {
            this.duplicateRequestFilter = duplicateRequestFilter;
            this.permitAll = permitAll;
            this.intervalTime = intervalTime;
            this.clearCachetime = clearCachetime;
        }

        // 存储参数和请求时间
        private Map<String, Long> requestCache = new ConcurrentHashMap<>();

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            boolean doFilter = true;
            // 使用 ContentCachingRequestWrapper 包装原始请求
            RepeatableReadHttpServletRequestWrapper wrappedRequest = null;
            if (this.duplicateRequestFilter) {
                // 判断请求路径是否需要放行
                boolean permit = false;
                if (CollectionUtil.isNotEmpty(this.permitAll)) {
                    for (RequestMatcher matcher: this.permitAll) {
                        if (matcher.matches(request)) {
                            permit = true;
                            break;
                        }
                    }
                }
                if (!permit) {
                    if (request instanceof RepeatableReadHttpServletRequestWrapper) {
                        wrappedRequest = (RepeatableReadHttpServletRequestWrapper) request;
                    } else {
                        wrappedRequest = new RepeatableReadHttpServletRequestWrapper(request);
                    }
                    doFilter = this.isValid(wrappedRequest);
                }
            }
            if (doFilter) {
                // 继续处理请求
                filterChain.doFilter(Objects.nonNull(wrappedRequest) ? wrappedRequest : request, response);
            } else {
                response.setContentType("application/json");
                response.setStatus(WebEndpointResponse.STATUS_TOO_MANY_REQUESTS);
    //            response.setStatus(HttpServletResponse.SC_OK);
    //            ObjectMapper mapper = new ObjectMapper();
    //            mapper.writeValue(response.getOutputStream(), R.error(WebEndpointResponse.STATUS_TOO_MANY_REQUESTS, "重复的请求"));
            }
        }

        /**
         * 验证请求的有效性（判断是否重复请求）
         * @param request
         * @return
         */
        private boolean isValid(RepeatableReadHttpServletRequestWrapper request) {
            boolean valid = true;
            // 缓存的key
            String key = TokenUtil.getUidByToken() + "_" + request.getServletPath() + "_" + request.getParamsToJSONString();
            // 获取之前的请求时间
            Long previousRequestTime = requestCache.get(key);
            if (previousRequestTime != null) {
                // 如果距离上次请求时间很短（例如1秒），则拒绝当前请求
                if (System.currentTimeMillis() - previousRequestTime < this.intervalTime) {
                    valid = false;
                }
            }
            this.clearOldRequests();
            // 缓存当前请求时间
            requestCache.put(key, System.currentTimeMillis());
            return valid;
        }

        // 用于清除缓存中的旧请求数据，防止缓存无限增长
        private void clearOldRequests() {
            requestCache.entrySet().removeIf(entry -> System.currentTimeMillis() - entry.getValue() > this.clearCachetime);
        }

    }

###### # 配置 OAuth2 资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103

    package xxx.config;

    import xxx.AuthExceptionEntryPoint;
    import xxx.CustomAccessDeniedHandler;
    import xxx.DuplicateRequestFilter;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

    import javax.servlet.Filter;
    import java.util.Arrays;
    import java.util.stream.Collectors;

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

        Logger log = LoggerFactory.getLogger(ResourceServerConfig.class);

        @Autowired
        private TokenStore tokenStore;

        /**
         * 是否开放所有接口
         */
        @Value("${http.security.permitAll:false}")
        private Boolean isPermitAll;

        /**
         * 是否启用重复请求过滤
         */
        @Value("${request.duplicateFilter.enabled:true}")
        private Boolean duplicateRequestFilter;

        /**
         * 间隔时间(毫秒)
         */
        @Value("${request.duplicateFilter.interval_time:1000}")
        private Long intervalTime;

        /**
         * 清除缓存时间(毫秒)
         */
        @Value("${request.duplicateFilter.clear_cache_time:30000}")
        private Long clearCachetime;

        /**
         * 不需要验证权限的接口
         */
        private String[] permitAll = new String[] {
                "/auth/getVCode", "/auth/login"
        };

        /**
         * 通行规则
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            HttpSecurity httpSecurity = http.csrf().disable();
            if (isPermitAll) {
                httpSecurity.authorizeRequests().antMatchers("/**").permitAll();
            } else {
                httpSecurity.authorizeRequests()
                        .antMatchers(permitAll).permitAll()
                        .antMatchers("/**").authenticated();
            }
            if (this.duplicateRequestFilter) {
                httpSecurity.addFilterAfter(duplicateRequestFilter(), AbstractPreAuthenticatedProcessingFilter.class);
            }
            //让X-frame-options失效,去除iframe限制
            http.headers().frameOptions().disable();
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.tokenStore(tokenStore).authenticationEntryPoint(new AuthExceptionEntryPoint())
                    .accessDeniedHandler(new CustomAccessDeniedHandler());

        }

        @Bean
        public Filter duplicateRequestFilter() {
            return new DuplicateRequestFilter(this.duplicateRequestFilter, Arrays.asList(this.permitAll)
                    .stream().map(AntPathRequestMatcher::new).collect(Collectors.toList()), this.intervalTime,
                    this.clearCachetime);
        }

    }

java 429