SpringBoot 接口防重實現:基于哈希算法的解決方案
作者:一安
在高并發或用戶誤操作場景下,接口重復提交會引發數據不一致、業務邏輯異常等問題。防重復提交就是防止用戶在短時間內對同一接口進行多次重復提交,導致數據重復創建或狀態異常。
引言
在高并發或用戶誤操作場景下,接口重復提交會引發數據不一致、業務邏輯異常等問題。防重復提交就是防止用戶在短時間內對同一接口進行多次重復提交,導致數據重復創建或狀態異常。
實現
圖片
方案原理
- 生成請求唯一標識:服務端接收請求后,根據
URL、請求參數等信息生成SHA-256哈希值,作為該請求的唯一標識。 - 緩存校驗:將哈希值存入緩存(如
Redis),并設置過期時間。若后續出現相同哈希值的請求,則判定為重復提交,直接攔截;若不存在,則放行并將哈希值存入緩存。
代碼實現
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/**
* 防重過期時間(單位:秒),默認 10 秒
*/
int expireSeconds() default 10;
}哈希工具類
public class HashUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 生成包含 JSON 體的請求哈希
* @param url 請求地址
* @param formParams 表單參數
* @param jsonBody JSON 請求體
*/
public static String generateSHA256(String url, Map<String, Object> formParams, String jsonBody) {
StringBuilder sb = new StringBuilder(url);
// 1. 加入排序后的表單參數
TreeMap<String, Object> sortedFormParams = new TreeMap<>(formParams);
for (Map.Entry<String, Object> entry : sortedFormParams.entrySet()) {
sb.append(entry.getKey()).append(entry.getValue());
}
// 2. 加入 JSON 體(若存在)
if (jsonBody != null && !jsonBody.isEmpty()) {
try {
// 將 JSON 轉為有序 Map 后拼接(避免 JSON 字段順序影響哈希)
Map<String, Object> jsonMap = objectMapper.readValue(jsonBody, Map.class);
TreeMap<String, Object> sortedJsonMap = new TreeMap<>(jsonMap);
sb.append(objectMapper.writeValueAsString(sortedJsonMap));
} catch (Exception e) {
// 解析失敗時直接拼接原始 JSON(可能存在順序問題,視業務容忍度調整)
sb.append(jsonBody);
}
}
return DigestUtils.sha256Hex(sb.toString());
}
}請求體包裝類
public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 讀取請求體并緩存
body = readBytes(request.getInputStream());
}
private byte[] readBytes(InputStream inputStream) throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
return outputStream.toByteArray();
}
}
@Override
public ServletInputStream getInputStream() {
// 返回緩存的請求體流
ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
returntrue;
}
@Override
public void setReadListener(ReadListener listener) {}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
// 獲取請求體字符串
public String getBody() {
return new String(body, StandardCharsets.UTF_8);
}
}請求體過濾器
@Component
public class RepeatableReadFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 僅包裝 HTTP 請求
if (request instanceof HttpServletRequest) {
ServletRequest wrapper = new RepeatableReadRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrapper, response);
} else {
chain.doFilter(request, response);
}
}
}防重攔截器
public class RepeatSubmitInterceptor implements HandlerInterceptor {
private final RedisTemplate<String, Object> redisTemplate;
public RepeatSubmitInterceptor(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
if (annotation != null) {
// 1. 提取 URL
String url = request.getRequestURI();
// 2. 提取表單參數
Map<String, Object> formParams = new HashMap<>();
request.getParameterMap().forEach((key, values) ->
formParams.put(key, values.length == 1 ? values[0] : values)
);
// 3. 提取 JSON 請求體(從包裝類中獲取)
String jsonBody = "";
if (request instanceof RepeatableReadRequestWrapper) {
jsonBody = ((RepeatableReadRequestWrapper) request).getBody();
}
// 4. 生成唯一哈希標識
String hash = HashUtil.generateSHA256(url, formParams, jsonBody);
// 5. 緩存校驗
Boolean exists = redisTemplate.opsForValue()
.setIfAbsent(hash, "1", annotation.expireSeconds(), java.util.concurrent.TimeUnit.SECONDS);
if (exists == null || !exists) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":400,\"msg\":\"請勿重復提交請求\"}");
returnfalse;
}
}
}
returntrue;
}
}配置攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final RedisTemplate<String, Object> redisTemplate;
public WebConfig(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Bean
public RepeatSubmitInterceptor repeatSubmitInterceptor() {
return new RepeatSubmitInterceptor(redisTemplate);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor())
.addPathPatterns("/**"); // 攔截所有接口,可根據需求調整
}
}責任編輯:武曉燕
來源:
一安未來





























