# 简介

由于前端会莫名其妙的对同一接口请求多次,从而占用后端资源造成浪费。所以采用了后端拦截相关重复请求的方案。此方案会将请求用户 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);
}

}