實(shí)戰(zhàn)出真知!SpringBoot 接口級(jí)防護(hù):限流、重放攻擊與簽名機(jī)制全解析
在當(dāng)今數(shù)字化服務(wù)廣泛開放的背景下,后端接口往往暴露在公網(wǎng)之下,極易成為攻擊者的突破口。接口調(diào)用的重放攻擊、參數(shù)偽造、暴力請(qǐng)求等問題屢見不鮮,嚴(yán)重威脅服務(wù)安全與數(shù)據(jù)完整性。
本文將基于 Spring Boot 框架,手把手實(shí)現(xiàn)一套可落地的接口安全防護(hù)機(jī)制,涵蓋簽名驗(yàn)證、防重放、限流控制等核心能力,適用于 B 端開放接口、系統(tǒng)對(duì)接場(chǎng)景。
接口簽名驗(yàn)證設(shè)計(jì)思路
簽名的目標(biāo)是確保「請(qǐng)求未被篡改 + 來源可信 + 有效時(shí)間內(nèi)唯一調(diào)用」。其基本設(shè)計(jì)流程如下:
請(qǐng)求參數(shù)說明
參數(shù)名 | 說明 |
| 接口調(diào)用方唯一標(biāo)識(shí) |
| 請(qǐng)求時(shí)間戳,用于防止過期請(qǐng)求 |
| 隨機(jī)字符串,確保請(qǐng)求唯一性 |
| 簽名值,由后端根據(jù)參數(shù)與秘鑰生成 |
簽名生成算法
簽名算法一般為:
sign = MD5(按 key 排序后的參數(shù)字符串 + appSecret)
參數(shù)拼接規(guī)則舉例:
appId=123&nonce=xyz&t=1710001234&data=xxx&appSecret=abcDEF123
接口限流與重放攻擊防護(hù)
限流實(shí)現(xiàn)(Redis 簡(jiǎn)單計(jì)數(shù)法)
String key = "rate_limit:" + normalizedPath + ":" + getClientIp(request);
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 10, TimeUnit.SECONDS);
}
if (count > 5) {
throw new ApiException("請(qǐng)求過于頻繁,請(qǐng)稍后再試");
}
重放防護(hù)(基于 nonce + timestamp)
String nonceKey = "nonce_cache:" + appId + ":" + nonce;
Boolean success = redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", 5, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(success)) {
throw new ApiException("重復(fù)請(qǐng)求被攔截");
}
Spring Boot + 自定義注解 + 攔截器實(shí)現(xiàn)簽名校驗(yàn)
接下來我們通過完整代碼實(shí)現(xiàn)接口簽名機(jī)制,確保項(xiàng)目中可直接落地。
步驟 1:定義簽名校驗(yàn)注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiSign {
boolean required() default true;
}
步驟 2:創(chuàng)建攔截器處理邏輯
@Component
public class ApiSecurityInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final long EXPIRE_TIME = 5 * 60; // 5分鐘
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) return true;
HandlerMethod method = (HandlerMethod) handler;
ApiSign apiSign = method.getMethodAnnotation(ApiSign.class);
if (apiSign == null || !apiSign.required()) return true;
// 提取參數(shù)
String appId = request.getParameter("appId");
String sign = request.getParameter("sign");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
if (StringUtils.isAnyBlank(appId, sign, nonce, timestamp)) {
throw new ApiException("簽名參數(shù)不完整");
}
// 校驗(yàn)時(shí)間戳
long currentTime = System.currentTimeMillis() / 1000;
if (Math.abs(currentTime - Long.parseLong(timestamp)) > EXPIRE_TIME) {
throw new ApiException("請(qǐng)求時(shí)間已過期");
}
// 重放校驗(yàn)
String nonceKey = "nonce_cache:" + appId + ":" + nonce;
if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", EXPIRE_TIME, TimeUnit.SECONDS))) {
throw new ApiException("重復(fù)請(qǐng)求被拒絕");
}
// 簽名驗(yàn)證
Map<String, String[]> parameterMap = request.getParameterMap();
String calculatedSign = SignUtils.calculateSign(parameterMap, getAppSecret(appId));
if (!sign.equalsIgnoreCase(calculatedSign)) {
throw new ApiException("簽名校驗(yàn)失敗");
}
return true;
}
private String getAppSecret(String appId) {
// 實(shí)際項(xiàng)目中建議從數(shù)據(jù)庫(kù)或配置中心加載
return "secretFor_" + appId;
}
}
步驟 3:簽名工具類封裝
public class SignUtils {
public static String calculateSign(Map<String, String[]> params, String appSecret) {
SortedMap<String, String> sortedParams = new TreeMap<>();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
if ("sign".equalsIgnoreCase(entry.getKey())) continue;
sortedParams.put(entry.getKey(), entry.getValue()[0]);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
sb.append("appSecret=").append(appSecret);
return DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
}
步驟 4:配置攔截器生效
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private ApiSecurityInterceptor apiSecurityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiSecurityInterceptor)
.addPathPatterns("/api/**");
}
}
步驟 5:接口使用方式示例
@GetMapping("/api/secure/data")
@ApiSign
public Result<String> getSecureData() {
return Result.success("這是簽名驗(yàn)證通過的數(shù)據(jù)");
}
請(qǐng)求調(diào)用流程說明
- 調(diào)用方生成帶簽名的請(qǐng)求;
- 請(qǐng)求通過限流判斷 → 重放判斷 → 參數(shù)完整性校驗(yàn);
- 后端根據(jù)參數(shù)生成服務(wù)端簽名與客戶端簽名對(duì)比;
- 簽名一致放行,失敗則拒絕。
總結(jié)
本方案結(jié)合 Spring Boot 提供了一套輕量、可擴(kuò)展的接口安全保護(hù)機(jī)制:
功能點(diǎn) | 技術(shù)實(shí)現(xiàn) |
簽名校驗(yàn) | 自定義注解 + 攔截器 + 參數(shù)排序 + MD5 簽名 |
防重放 | Redis 緩存 |
限流 | Redis 計(jì)數(shù)器,單位時(shí)間請(qǐng)求次數(shù)限制 |
易用性 | 注解式接入,低侵入、接口層無需重復(fù)代碼 |
可擴(kuò)展性 | 可集成 AES 加密、IP 白名單、權(quán)限控制等高級(jí)能力 |
適用于中臺(tái)接口、對(duì)接系統(tǒng)、開放平臺(tái)等場(chǎng)景。可以作為統(tǒng)一接口安全網(wǎng)關(guān)的重要組成部分,也可獨(dú)立部署在 Spring Boot 服務(wù)中。