SpringBoot實現(xiàn)防重放攻擊的五種方案
一、重放攻擊基本概念
1.1 什么是重放攻擊
重放攻擊是一種網(wǎng)絡(luò)攻擊手段,攻擊者截獲一個有效的數(shù)據(jù)傳輸,然后在稍后的時間重新發(fā)送相同的數(shù)據(jù),以實現(xiàn)欺騙系統(tǒng)的目的。在Web應(yīng)用中,這通常表現(xiàn)為重復(fù)提交相同的請求,比如:
- 重復(fù)提交訂單付款請求
- 重復(fù)使用過期的訪問令牌
- 重復(fù)提交表單數(shù)據(jù)
- 重新發(fā)送包含認(rèn)證信息的請求
1.2 重放攻擊的危害
重放攻擊可能導(dǎo)致以下安全問題:
- 資金損失:重復(fù)執(zhí)行支付交易
- 資源耗盡:大量重復(fù)請求導(dǎo)致系統(tǒng)資源枯竭
- 數(shù)據(jù)不一致:重復(fù)提交導(dǎo)致數(shù)據(jù)重復(fù)或狀態(tài)混亂
- 業(yè)務(wù)邏輯被繞過:繞過設(shè)計中的業(yè)務(wù)規(guī)則
- 權(quán)限提升:復(fù)用他人有效的認(rèn)證信息
二、時間戳+請求超時機(jī)制
2.1 基本原理
這種方案要求客戶端在每個請求中附帶當(dāng)前時間戳,服務(wù)器收到請求后,檢查時間戳是否在允許的時間窗口內(nèi)(通常為幾分鐘)。
如果請求的時間戳超出時間窗口,則認(rèn)為是過期請求或潛在的重放攻擊,拒絕處理該請求。
2.2 SpringBoot實現(xiàn)
首先,創(chuàng)建一個請求包裝類,包含時間戳字段:
@Data
public class ApiRequest<T> {
private Long timestamp; // 請求時間戳,毫秒級
private T data; // 實際請求數(shù)據(jù)
}
然后,創(chuàng)建一個攔截器來檢查請求時間戳:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestBodyCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (isRequestBodyEligible(request)) {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
} else {
filterChain.doFilter(request, response);
}
}
private boolean isRequestBodyEligible(HttpServletRequest request) {
String method = request.getMethod();
return "POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method) || "DELETE".equals(method);
}
}
@Component
@Slf4j
public class TimestampInterceptor implements HandlerInterceptor {
private static final long ALLOWED_TIME_WINDOW = 5 * 60 * 1000; // 5分鐘時間窗口
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 檢查是否需要進(jìn)行時間戳驗證
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (!handlerMethod.getMethod().isAnnotationPresent(CheckTimestamp.class)) {
return true;
}
// 獲取請求體
String requestBody = getRequestBody(request);
try {
// 解析請求體,獲取時間戳
ApiRequest<?> apiRequest = new ObjectMapper().readValue(requestBody, ApiRequest.class);
Long timestamp = apiRequest.getTimestamp();
if (timestamp == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Missing timestamp");
return false;
}
// 檢查時間戳是否在允許的時間窗口內(nèi)
long currentTime = System.currentTimeMillis();
if (currentTime - timestamp > ALLOWED_TIME_WINDOW || timestamp > currentTime) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Request expired or invalid timestamp");
return false;
}
return true;
} catch (Exception e) {
log.error("Error processing timestamp", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid request format");
return false;
}
}
private String getRequestBody(HttpServletRequest request) throws IOException {
// 針對ContentCachingRequestWrapper的處理
if (request instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
// 讀取緩存的內(nèi)容
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
return new String(buf, wrapper.getCharacterEncoding());
}
}
// 針對MultiReadHttpServletRequest的處理
try (BufferedReader reader = request.getReader()) {
return reader.lines().collect(Collectors.joining(System.lineSeparator()));
}
}
}
創(chuàng)建一個注解,用于標(biāo)記需要進(jìn)行時間戳驗證的接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckTimestamp {
}
配置攔截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TimestampInterceptor timestampInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timestampInterceptor)
.addPathPatterns("/api/**");
}
}
在需要防止重放攻擊的接口上添加注解:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@CheckTimestamp
public ResponseEntity<?> createOrder(@RequestBody ApiRequest<OrderCreateRequest> request) {
// 處理訂單創(chuàng)建邏輯
return ResponseEntity.ok().build();
}
}
2.3 優(yōu)缺點分析
優(yōu)點:
- 實現(xiàn)簡單,無需額外存儲
- 不依賴會話狀態(tài),適合分布式系統(tǒng)
- 客戶端實現(xiàn)簡單,只需添加時間戳
缺點:
- 需要客戶端和服務(wù)器時間同步
- 時間窗口存在權(quán)衡:太短影響用戶體驗,太長降低安全性
- 無法防止時間窗口內(nèi)的重放攻擊
- 不適合時間敏感的高安全場景
三、Nonce隨機(jī)數(shù)+Redis緩存
3.1 基本原理
Nonce(Number used once)是一個只使用一次的隨機(jī)數(shù)。在此方案中,客戶端每次請求都生成一個唯一的隨機(jī)數(shù),并發(fā)送給服務(wù)器。
服務(wù)器將使用過的Nonce存儲在Redis緩存中一段時間,拒絕任何使用重復(fù)Nonce的請求。這種方式可以有效防止重放攻擊,因為每個有效請求都需要一個從未使用過的Nonce。
3.2 SpringBoot實現(xiàn)
首先,擴(kuò)展請求包裝類,添加Nonce字段:
@Data
public class ApiRequest<T> {
private Long timestamp; // 請求時間戳,毫秒級
private String nonce; // 隨機(jī)數(shù),每次請求唯一
private T data; // 實際請求數(shù)據(jù)
}
添加Redis配置:
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
創(chuàng)建Nonce檢查攔截器:
@Component
@Slf4j
public class NonceInterceptor implements HandlerInterceptor {
private static final long NONCE_EXPIRE_SECONDS = 3600; // Nonce有效期1小時
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (!handlerMethod.getMethod().isAnnotationPresent(CheckNonce.class)) {
return true;
}
String requestBody = getRequestBody(request);
try {
ApiRequest<?> apiRequest = new ObjectMapper().readValue(requestBody, ApiRequest.class);
String nonce = apiRequest.getNonce();
if (StringUtils.isEmpty(nonce)) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Missing nonce");
return false;
}
// 檢查時間戳
Long timestamp = apiRequest.getTimestamp();
if (timestamp == null || System.currentTimeMillis() - timestamp > 5 * 60 * 1000) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Request expired or invalid timestamp");
return false;
}
// 檢查Nonce是否已使用
String nonceKey = "nonce:" + nonce;
Boolean isFirstUse = redisTemplate.opsForValue().setIfAbsent(nonceKey, timestamp.toString(), NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS);
if (isFirstUse == null || !isFirstUse) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Duplicate request or replay attack detected");
log.warn("Duplicate nonce detected: {}", nonce);
return false;
}
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid request format");
return false;
}
}
// getRequestBody方法同上
}
創(chuàng)建注解,標(biāo)記需要進(jìn)行Nonce驗證的接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckNonce {
}
在需要防止重放攻擊的接口上添加注解:
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
@PostMapping
@CheckNonce
public ResponseEntity<?> processPayment(@RequestBody ApiRequest<PaymentRequest> request) {
// 處理支付邏輯
return ResponseEntity.ok().build();
}
}
3.3 客戶端生成Nonce示例
public class ApiClient {
public static ApiRequest<T> createRequest(T data) {
ApiRequest<T> request = new ApiRequest<>();
request.setTimestamp(System.currentTimeMillis());
request.setNonce(UUID.randomUUID().toString());
request.setData(data);
return request;
}
}
3.4 優(yōu)缺點分析
優(yōu)點:
- 安全性高,每個請求都必須使用唯一的Nonce
- 可以有效防止在任何時間窗口內(nèi)的重放攻擊
- 結(jié)合時間戳可以雙重保障
缺點:
- 需要存儲使用過的Nonce,增加系統(tǒng)復(fù)雜性
- 在分布式系統(tǒng)中需要共享Nonce存儲
- 對Redis等存儲系統(tǒng)有依賴
- 客戶端需要生成唯一Nonce,實現(xiàn)相對復(fù)雜
四、冪等性令牌機(jī)制
4.1 基本原理
冪等性令牌機(jī)制是一種專門針對非冪等操作(如創(chuàng)建訂單、支付等)設(shè)計的防重放方案。
服務(wù)器先生成一個一次性的令牌并提供給客戶端,客戶端在執(zhí)行操作時必須提交這個令牌,服務(wù)器驗證令牌有效后執(zhí)行操作并立即使令牌失效,從而保證操作不會重復(fù)執(zhí)行。
4.2 SpringBoot實現(xiàn)
首先,創(chuàng)建令牌服務(wù):
@Service
@Slf4j
public class IdempotencyTokenService {
private static final String TOKEN_PREFIX = "idempotency_token:";
private static final long TOKEN_EXPIRE_MINUTES = 30; // 令牌有效期30分鐘
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 生成冪等性令牌
*/
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(
TOKEN_PREFIX + token,
"UNUSED",
TOKEN_EXPIRE_MINUTES,
TimeUnit.MINUTES
);
return token;
}
/**
* 驗證并消費令牌
* @return true表示令牌有效且已成功消費,false表示令牌無效或已被消費
*/
public boolean validateAndConsumeToken(String token) {
if (StringUtils.isEmpty(token)) {
return false;
}
String key = TOKEN_PREFIX + token;
// 使用Redis的原子操作驗證并更新令牌狀態(tài)
String script = "if redis.call('get', KEYS[1]) == 'UNUSED' then "
+ "redis.call('set', KEYS[1], 'USED') "
+ "return 1 "
+ "else "
+ "return 0 "
+ "end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key)
);
return result != null && result == 1;
}
}
創(chuàng)建獲取令牌的API:
@RestController
@RequestMapping("/api/tokens")
public class TokenController {
@Autowired
private IdempotencyTokenService tokenService;
@PostMapping
public ResponseEntity<Map<String, String>> generateToken() {
String token = tokenService.generateToken();
Map<String, String> response = Collections.singletonMap("token", token);
return ResponseEntity.ok(response);
}
}
創(chuàng)建冪等性檢查攔截器:
@Component
@Slf4j
public class IdempotencyInterceptor implements HandlerInterceptor {
@Autowired
private IdempotencyTokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
IdempotentOperation annotation = handlerMethod.getMethod().getAnnotation(IdempotentOperation.class);
if (annotation == null) {
return true;
}
// 從請求頭獲取冪等性令牌
String token = request.getHeader("Idempotency-Token");
if (StringUtils.isEmpty(token)) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Missing idempotency token");
return false;
}
// 驗證并消費令牌
boolean isValid = tokenService.validateAndConsumeToken(token);
if (!isValid) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid or already used idempotency token");
log.warn("Attempt to reuse idempotency token: {}", token);
return false;
}
return true;
}
}
創(chuàng)建冪等性操作注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IdempotentOperation {
}
在需要保證冪等性的API上使用注解:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
@IdempotentOperation
public ResponseEntity<?> createOrder(@RequestBody OrderCreateRequest request) {
// 創(chuàng)建訂單
OrderDTO order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}
}
4.3 客戶端使用示例
// 第一步: 獲取冪等性令牌
const tokenResponse = await fetch('/api/tokens', { method: 'POST' });
const { token } = await tokenResponse.json();
// 第二步: 使用令牌提交請求
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Token': token
},
body: JSON.stringify({
// 訂單數(shù)據(jù)
})
});
4.4 優(yōu)缺點分析
優(yōu)點:
- 專為非冪等操作設(shè)計,安全性高
- 客戶端必須先獲取令牌,可以有效防止未授權(quán)請求
- 服務(wù)端控制令牌生成和驗證,安全可控
- 可以與業(yè)務(wù)邏輯完美結(jié)合
缺點:
- 需要額外的獲取令牌請求,增加交互復(fù)雜性
- 依賴外部存儲系統(tǒng)保存令牌狀態(tài)
- 對客戶端有特定要求,實現(xiàn)相對復(fù)雜
- 令牌有效期管理需要權(quán)衡
五、請求簽名認(rèn)證
5.1 基本原理
請求簽名認(rèn)證方案通過對請求參數(shù)、時間戳、隨機(jī)數(shù)等信息進(jìn)行加密簽名,確保請求在傳輸過程中不被篡改,同時結(jié)合時間戳和隨機(jī)數(shù)防止重放攻擊。
該方案通常用于API安全性要求較高的場景,如支付、金融等領(lǐng)域。
5.2 SpringBoot實現(xiàn)
首先,創(chuàng)建請求簽名工具類:
@Component
public class SignatureUtils {
/**
* 生成簽名
* @param params 參與簽名的參數(shù)
* @param timestamp 時間戳
* @param nonce 隨機(jī)數(shù)
* @param secretKey 密鑰
* @return 簽名
*/
public String generateSignature(Map<String, String> params, long timestamp, String nonce, String secretKey) {
// 1. 按參數(shù)名ASCII碼排序
Map<String, String> sortedParams = new TreeMap<>(params);
// 2. 構(gòu)建簽名字符串
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (StringUtils.isNotEmpty(entry.getValue())) {
builder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
// 3. 添加時間戳和隨機(jī)數(shù)
builder.append("timestamp=").append(timestamp).append("&");
builder.append("nonce=").append(nonce).append("&");
builder.append("key=").append(secretKey);
// 4. 進(jìn)行MD5簽名
return DigestUtils.md5DigestAsHex(builder.toString().getBytes(StandardCharsets.UTF_8));
}
/**
* 驗證簽名
*/
public boolean verifySignature(Map<String, String> params, long timestamp, String nonce, String signature, String secretKey) {
String calculatedSignature = generateSignature(params, timestamp, nonce, secretKey);
return calculatedSignature.equals(signature);
}
}
創(chuàng)建簽名驗證攔截器:
@Component
@Slf4j
public class SignatureInterceptor implements HandlerInterceptor {
private static final long ALLOWED_TIME_WINDOW = 5 * 60 * 1000; // 5分鐘時間窗口
@Autowired
private SignatureUtils signatureUtils;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Value("${api.secret-key}")
private String secretKey;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (!handlerMethod.getMethod().isAnnotationPresent(CheckSignature.class)) {
return true;
}
try {
// 1. 獲取請求頭中的簽名信息
String signature = request.getHeader("X-Signature");
String timestampStr = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
String appId = request.getHeader("X-App-Id");
if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestampStr) ||
StringUtils.isEmpty(nonce) || StringUtils.isEmpty(appId)) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Missing signature parameters");
return false;
}
// 2. 檢查時間戳是否在允許的時間窗口內(nèi)
long timestamp = Long.parseLong(timestampStr);
long currentTime = System.currentTimeMillis();
if (currentTime - timestamp > ALLOWED_TIME_WINDOW || timestamp > currentTime) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Request expired or invalid timestamp");
return false;
}
// 3. 檢查nonce是否已使用
String nonceKey = "signature_nonce:" + nonce;
Boolean isFirstUse = redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", ALLOWED_TIME_WINDOW, TimeUnit.MILLISECONDS);
if (isFirstUse == null || !isFirstUse) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Duplicate request or replay attack detected");
log.warn("Duplicate nonce detected: {}", nonce);
return false;
}
// 4. 獲取請求參數(shù)
Map<String, String> params = new HashMap<>();
// 從請求體或URL參數(shù)中獲取參數(shù)...
if (request.getMethod().equals("GET")) {
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, values[0]);
}
});
} else {
// 解析請求體,這里簡化處理
String requestBody = getRequestBody(request);
if (StringUtils.isNotEmpty(requestBody)) {
try {
Map<String, Object> bodyMap = new ObjectMapper().readValue(requestBody, Map.class);
bodyMap.forEach((key, value) -> {
if (value != null) {
params.put(key, value.toString());
}
});
} catch (Exception e) {
log.error("Failed to parse request body", e);
}
}
}
// 5. 驗證簽名
boolean isValid = signatureUtils.verifySignature(params, timestamp, nonce, signature, secretKey);
if (!isValid) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid signature");
log.warn("Invalid signature detected for appId: {}", appId);
return false;
}
return true;
} catch (Exception e) {
log.error("Signature verification error", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Signature verification failed");
return false;
}
}
// getRequestBody方法同上
}
創(chuàng)建簽名驗證注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckSignature {
}
在需要簽名驗證的API上使用注解:
@RestController
@RequestMapping("/api/secure")
public class SecureApiController {
@GetMapping("/data")
@CheckSignature
public ResponseEntity<?> getSecureData() {
// 處理安全數(shù)據(jù)
return ResponseEntity.ok(Map.of("data", "Secure data"));
}
@PostMapping("/transaction")
@CheckSignature
public ResponseEntity<?> processTransaction(@RequestBody TransactionRequest request) {
// 處理交易
return ResponseEntity.ok(Map.of("result", "success"));
}
}
5.3 客戶端簽名示例
public class ApiClient {
private static final String APP_ID = "your-app-id";
private static final String SECRET_KEY = "your-secret-key";
public static <T> String callSecureApi(String url, T requestBody, HttpMethod method) throws Exception {
// 1. 準(zhǔn)備簽名參數(shù)
long timestamp = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString();
// 2. 請求參數(shù)轉(zhuǎn)換為Map
Map<String, String> params = new HashMap<>();
if (requestBody != null) {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(requestBody);
Map<String, Object> map = mapper.readValue(json, Map.class);
map.forEach((key, value) -> {
if (value != null) {
params.put(key, value.toString());
}
});
}
// 3. 生成簽名
String signature = generateSignature(params, timestamp, nonce, SECRET_KEY);
// 4. 發(fā)起請求
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(method.name());
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("X-App-Id", APP_ID);
connection.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
connection.setRequestProperty("X-Nonce", nonce);
connection.setRequestProperty("X-Signature", signature);
// 設(shè)置請求體...
// 獲取響應(yīng)...
return "Response";
}
private static String generateSignature(Map<String, String> params, long timestamp, String nonce, String secretKey) {
// 實現(xiàn)與服務(wù)端相同的簽名算法
// ...
}
}
5.4 優(yōu)缺點分析
優(yōu)點:
- 安全性高,可以同時防止重放攻擊和請求篡改
- 客戶端無需事先獲取token,減少交互
- 支持多種請求方式(GET/POST等)
- 適合第三方API調(diào)用場景
缺點:
- 實現(xiàn)復(fù)雜,客戶端和服務(wù)端需要一致的簽名算法
- 調(diào)試?yán)щy,簽名錯誤不易排查
- 需要安全地管理密鑰
- 計算簽名有一定性能開銷
六、分布式鎖防重復(fù)提交
6.1 基本原理
分布式鎖是一種常用的并發(fā)控制機(jī)制,可以用來防止重復(fù)提交。
當(dāng)收到請求時,系統(tǒng)嘗試獲取一個基于請求特征(如用戶ID+操作類型)的分布式鎖,如果獲取成功則處理請求,否則拒絕請求。
這種方式特別適合防止用戶在短時間內(nèi)多次點擊提交按鈕導(dǎo)致的重復(fù)提交問題。
6.2 SpringBoot實現(xiàn)
首先,添加Redis依賴并配置Redisson客戶端:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(RedisProperties redisProperties) {
Config config = new Config();
String redisUrl = String.format("redis://%s:%d", redisProperties.getHost(), redisProperties.getPort());
config.useSingleServer()
.setAddress(redisUrl)
.setPassword(redisProperties.getPassword())
.setDatabase(redisProperties.getDatabase());
return Redisson.create(config);
}
}
創(chuàng)建分布式鎖服務(wù):
@Service
@Slf4j
public class DistributedLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 嘗試獲取鎖
* @param lockKey 鎖的鍵
* @param waitTime 等待獲取鎖的最長時間
* @param leaseTime 持有鎖的時間
* @return 是否獲取到鎖
*/
public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Thread interrupted while trying to acquire lock", e);
return false;
}
}
/**
* 釋放鎖
*/
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
創(chuàng)建防重復(fù)提交注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
/**
* 鎖的前綴
*/
String prefix() default "duplicate_check:";
/**
* 等待獲取鎖的時間(毫秒)
*/
long waitTime() default 0;
/**
* 持有鎖的時間(毫秒)
*/
long leaseTime() default 5000;
/**
* 鎖的Key的SpEL表達(dá)式
*/
String key() default "";
}
創(chuàng)建防重復(fù)提交切面:
@Aspect
@Component
@Slf4j
public class DuplicateSubmitAspect {
@Autowired
private DistributedLockService lockService;
@Around("@annotation(preventDuplicateSubmit)")
public Object around(ProceedingJoinPoint point, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
// 獲取請求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return point.proceed();
}
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
// 獲取當(dāng)前用戶ID(實際項目中應(yīng)從認(rèn)證信息獲?。?
String userId = getUserId(request);
// 構(gòu)建鎖的key
String lockKey;
if (StringUtils.isNotEmpty(preventDuplicateSubmit.key())) {
// 使用SpEL表達(dá)式解析key
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("request", request);
// 添加方法參數(shù)到上下文
MethodSignature signature = (MethodSignature) point.getSignature();
String[] parameterNames = signature.getParameterNames();
Object[] args = point.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
Expression expression = new SpelExpressionParser().parseExpression(preventDuplicateSubmit.key());
lockKey = preventDuplicateSubmit.prefix() + expression.getValue(context, String.class);
} else {
// 默認(rèn)使用用戶ID + URI + 方法作為鎖key
lockKey = preventDuplicateSubmit.prefix() + userId + ":" + requestURI + ":" + requestMethod;
}
// 嘗試獲取鎖
boolean locked = lockService.tryLock(lockKey, preventDuplicateSubmit.waitTime(),
preventDuplicateSubmit.leaseTime(), TimeUnit.MILLISECONDS);
if (!locked) {
log.warn("Duplicate submit detected. userId: {}, uri: {}", userId, requestURI);
throw new DuplicateSubmitException("請勿重復(fù)提交");
}
try {
// 執(zhí)行實際方法
return point.proceed();
} finally {
// 釋放鎖
lockService.unlock(lockKey);
}
}
private String getUserId(HttpServletRequest request) {
// 實際項目中應(yīng)從認(rèn)證信息獲取用戶ID
// 這里簡化處理,從請求頭或會話中獲取
String userId = request.getHeader("X-User-Id");
if (StringUtils.isEmpty(userId)) {
// 如果請求頭中沒有,嘗試從會話中獲取
HttpSession session = request.getSession(false);
if (session != null) {
Object userObj = session.getAttribute("user");
if (userObj != null) {
// 假設(shè)user對象有g(shù)etId方法
// userId = ((User) userObj).getId();
userId = "demo-user";
}
}
}
// 如果仍然沒有用戶ID,使用IP地址作為標(biāo)識
if (StringUtils.isEmpty(userId)) {
userId = request.getRemoteAddr();
}
return userId;
}
}
創(chuàng)建異常類:
public class DuplicateSubmitException extends RuntimeException {
public DuplicateSubmitException(String message) {
super(message);
}
}
創(chuàng)建全局異常處理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateSubmitException.class)
public ResponseEntity<Map<String, String>> handleDuplicateSubmitException(DuplicateSubmitException e) {
Map<String, String> error = new HashMap<>();
error.put("error", "duplicate_submit");
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(error);
}
}
在需要防重復(fù)提交的接口上使用注解:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@PreventDuplicateSubmit(leaseTime = 5000)
public ResponseEntity<?> submitOrder(@RequestBody OrderRequest request) {
// 處理訂單提交
return ResponseEntity.ok(Map.of("orderId", "123456"));
}
@PostMapping("/complex")
@PreventDuplicateSubmit(key = "#request.productId + ':' + #request.userId")
public ResponseEntity<?> submitComplexOrder(@RequestBody OrderRequest request) {
// 處理復(fù)雜訂單提交
return ResponseEntity.ok(Map.of("orderId", "123456"));
}
}
6.3 優(yōu)缺點分析
優(yōu)點:
- 可以有效防止短時間內(nèi)的重復(fù)提交
- 支持基于業(yè)務(wù)屬性的鎖定,靈活性高
- 使用簡單,只需添加注解
- 可以與業(yè)務(wù)邏輯解耦
缺點:
- 依賴外部分布式鎖系統(tǒng)
- 鎖的粒度和超時時間需要仔細(xì)設(shè)計
- 可能導(dǎo)致正常請求被誤判為重復(fù)提交
- 需要正確處理鎖的釋放,避免死鎖
七、方案對比
| 防重放方案 | 安全級別 | 實現(xiàn)復(fù)雜度 | 性能影響 | 分布式支持 | 客戶端配合 | 適用場景 |
|---|---|---|---|---|---|---|
| 時間戳+超時機(jī)制 | 低 | 簡單 | 低 | 好 | 簡單 | 一般API,低安全需求 |
| Nonce+Redis緩存 | 高 | 中等 | 中 | 好 | 中等 | 安全敏感API |
| 冪等性令牌機(jī)制 | 高 | 中等 | 中 | 好 | 復(fù)雜 | 非冪等操作,如支付 |
| 請求簽名認(rèn)證 | 極高 | 復(fù)雜 | 中高 | 好 | 復(fù)雜 | 第三方API,金融接口 |
| 分布式鎖防重復(fù)提交 | 中 | 中等 | 中 | 好 | 無需 | 表單提交,用戶操作 |
八、總結(jié)
在實際應(yīng)用中,往往需要組合使用多種防重放策略,實施分層防護(hù),并與業(yè)務(wù)邏輯緊密結(jié)合,才能構(gòu)建出既安全又易用的系統(tǒng)。
防重放攻擊只是Web安全的一個方面,還應(yīng)關(guān)注其他安全威脅,如XSS、CSRF、SQL注入等,綜合提升系統(tǒng)的安全性。
以上就是SpringBoot實現(xiàn)防重放攻擊的五種方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot防重放攻擊的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實現(xiàn)LeetCode(1.兩數(shù)之和)
這篇文章主要介紹了Java實現(xiàn)LeetCode(兩數(shù)之和),本文使用java采用多種發(fā)放實現(xiàn)了LeetCode的兩數(shù)之和題目,需要的朋友可以參考下2021-06-06
SpringBoot實現(xiàn)Logback輸出日志到Kafka方式
本文介紹了如何在SpringBoot應(yīng)用中通過自定義Appender實現(xiàn)Logback輸出日志到Kafka,包括配置maven依賴、Kafka工具類和logback.xml配置2025-02-02
淺談Java分布式架構(gòu)下如何實現(xiàn)分布式鎖
這篇文章主要介紹了淺談Java分布式架構(gòu)下如何實現(xiàn)分布式鎖,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07

