# 简介
由于前端会莫名其妙的对同一接口请求多次,从而占用后端资源造成浪费。所以采用了后端拦截相关重复请求的方案。此方案会将请求用户 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 { 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 { return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(requestBody))); }
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()) { 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; }
private boolean isJsonRequest() { String contentType = this.getContentType(); return contentType != null && contentType.toLowerCase().contains("application/json"); }
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; 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; 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);
} }
private boolean isValid(RepeatableReadHttpServletRequestWrapper request) { boolean valid = true; String key = TokenUtil.getUidByToken() + "_" + request.getServletPath() + "_" + request.getParamsToJSONString(); Long previousRequestTime = requestCache.get(key); if (previousRequestTime != null) { 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" };
@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); } 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); }
}
|