Spring Boot 接口安全設(shè)計(jì):接口限流、防重放攻擊與簽名驗(yàn)證實(shí)戰(zhàn)
前言
在當(dāng)今互聯(lián)網(wǎng)應(yīng)用開發(fā)中,接口安全至關(guān)重要。對(duì)于Spring Boot項(xiàng)目而言,保障接口不被惡意調(diào)用、數(shù)據(jù)不被篡改、請(qǐng)求不被重放,是后端開發(fā)者必須攻克的安全難題。
本文將深入探討如何設(shè)計(jì)并實(shí)現(xiàn)一套安全、通用、可落地的接口安全方案,涵蓋接口限流、防重放攻擊與簽名驗(yàn)證等關(guān)鍵技術(shù)。
效果圖
圖片
接口限流
為什么需要接口限流
在高并發(fā)場(chǎng)景下,接口可能面臨大量請(qǐng)求的沖擊。如果不加以控制,可能導(dǎo)致服務(wù)器資源耗盡,服務(wù)響應(yīng)變慢甚至崩潰。接口限流的主要目的包括:
- 保護(hù)后端服務(wù):防止某個(gè)接口被惡意請(qǐng)求或突發(fā)流量擊垮,確保后端服務(wù)的穩(wěn)定性。
- 防止濫用:限制單個(gè)用戶或客戶端對(duì)接口的訪問頻率,避免惡意刷接口行為。
- 節(jié)省資源:合理控制流量,保護(hù)后端數(shù)據(jù)庫(kù)、緩存等資源,提高系統(tǒng)整體性能。
限流算法
常見的限流算法有以下幾種:
- 令牌桶算法(
Token Bucket):系統(tǒng)按固定速率生成令牌放入桶中,桶有固定容量。客戶端請(qǐng)求時(shí)需要從桶中獲取令牌,若桶中有足夠令牌則請(qǐng)求通過,否則請(qǐng)求被拒絕。例如,每秒生成10個(gè)令牌,桶容量為100,意味著系統(tǒng)允許一定程度的突發(fā)流量,但長(zhǎng)期平均下來每秒處理10個(gè)請(qǐng)求。 - 漏桶算法(
Leaky Bucket):請(qǐng)求像水流一樣進(jìn)入一個(gè)固定容量的桶中,桶以固定速率處理請(qǐng)求(漏水),超出桶容量的請(qǐng)求將被丟棄。該算法能保證請(qǐng)求以固定速率被處理,但無法應(yīng)對(duì)突發(fā)流量。 - 滑動(dòng)窗口計(jì)數(shù)器法(
Sliding Window Counter):將時(shí)間劃分為多個(gè)固定大小的窗口,每個(gè)窗口記錄請(qǐng)求數(shù)量。隨著時(shí)間推移,窗口滑動(dòng),通過統(tǒng)計(jì)滑動(dòng)窗口內(nèi)的請(qǐng)求總數(shù)來判斷是否限流。與簡(jiǎn)單的固定窗口計(jì)數(shù)器法相比,滑動(dòng)窗口法能更細(xì)粒度地控制流量,避免在窗口切換時(shí)出現(xiàn)流量突增導(dǎo)致的限流失效問題。
實(shí)現(xiàn)接口限流示例
public class RateLimiterExample {
// 創(chuàng)建一個(gè)RateLimiter,每秒允許10個(gè)請(qǐng)求
private static final RateLimiter rateLimiter = RateLimiter.create(10);
public static boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
}在接口方法中,可以通過調(diào)用tryAcquire方法來判斷是否允許請(qǐng)求通過:
@RestController
public class ExampleController {
@GetMapping("/example")
public ResponseEntity<String> example() {
if (!RateLimiterExample.tryAcquire()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("請(qǐng)求過于頻繁,請(qǐng)稍后再試");
}
// 處理正常業(yè)務(wù)邏輯
return ResponseEntity.ok("成功響應(yīng)");
}
}另外,也可以使用Spring AOP(面向切面編程)結(jié)合自定義注解來實(shí)現(xiàn)更靈活的接口限流。通過自定義注解標(biāo)記需要限流的接口,在切面類中使用限流邏輯對(duì)標(biāo)記的接口進(jìn)行攔截和處理,實(shí)現(xiàn)統(tǒng)一的限流控制。
防重放攻擊
重放攻擊是指攻擊者截獲并記錄合法用戶的有效請(qǐng)求,然后在稍后的時(shí)間重新發(fā)送這些請(qǐng)求,以達(dá)到欺騙系統(tǒng)的目的。這種攻擊在涉及交易、數(shù)據(jù)修改等場(chǎng)景中危害較大,可能導(dǎo)致數(shù)據(jù)重復(fù)處理、資金損失等問題。
防重放攻擊的方案
為了防止重放攻擊,可以采用以下幾種常見方案:
- 時(shí)間戳(
timestamp) + 有效時(shí)間窗口:在請(qǐng)求中添加時(shí)間戳參數(shù),服務(wù)器接收到請(qǐng)求后,判斷時(shí)間戳與當(dāng)前時(shí)間的差值是否在有效時(shí)間窗口內(nèi)(例如5分鐘)。如果超出窗口,則認(rèn)為請(qǐng)求已過期,拒絕處理。這種方式可以有效防止攻擊者在較長(zhǎng)時(shí)間后重放請(qǐng)求,但對(duì)于短時(shí)間內(nèi)的重放攻擊防護(hù)較弱。 - 隨機(jī)數(shù)(
nonce)去重機(jī)制:請(qǐng)求中攜帶一個(gè)唯一的隨機(jī)數(shù)(nonce),服務(wù)器記錄每次請(qǐng)求的nonce值。當(dāng)接收到新請(qǐng)求時(shí),檢查該nonce是否已存在。若存在,則判定為重復(fù)請(qǐng)求,拒絕處理。為了避免存儲(chǔ)大量nonce值導(dǎo)致內(nèi)存占用過高,可以結(jié)合時(shí)間戳,僅存儲(chǔ)有效時(shí)間窗口內(nèi)的nonce值。
防止重放攻擊示例
public class ReplayAttackInterceptor implements HandlerInterceptor {
private static final Set<String> nonceSet = ConcurrentHashMap.newKeySet();
private static final long EXPIRE_TIME = 5 * 60; // 5分鐘有效期,單位秒
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String appId = request.getHeader("appId");
String nonce = request.getHeader("nonce");
String timestamp = request.getHeader("timestamp");
if (appId == null || nonce == null || timestamp == null) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
returnfalse;
}
long currentTime = System.currentTimeMillis() / 1000;
if (currentTime - Long.parseLong(timestamp) > EXPIRE_TIME) {
response.setStatus(HttpStatus.REQUEST_TIMEOUT.value());
returnfalse;
}
String key = appId + nonce;
if (nonceSet.contains(key)) {
response.setStatus(HttpStatus.CONFLICT.value());
returnfalse;
}
nonceSet.add(key);
// 設(shè)置過期時(shí)間,避免nonceSet無限增長(zhǎng)
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService.schedule(() -> nonceSet.remove(key), EXPIRE_TIME, TimeUnit.SECONDS);
executorService.shutdown();
returntrue;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 處理后邏輯,可空
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 完成后邏輯,可空
}
}注冊(cè)攔截器:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ReplayAttackInterceptor())
.addPathPatterns("/**"); // 攔截所有接口
}
}簽名驗(yàn)證
為什么需要簽名機(jī)制
在接口調(diào)用過程中,簽名機(jī)制用于驗(yàn)證請(qǐng)求的合法性和完整性,防止接口被惡意調(diào)用、參數(shù)被篡改等問題。常見的安全風(fēng)險(xiǎn)包括:
- 接口被惡意刷爆:攻擊者偽造大量請(qǐng)求,不斷調(diào)用接口,導(dǎo)致服務(wù)器資源耗盡。
- 請(qǐng)求參數(shù)被篡改:中間人在請(qǐng)求傳輸過程中修改請(qǐng)求參數(shù),獲取非法利益。
- 敏感參數(shù)泄露:接口參數(shù)暴露,可能導(dǎo)致敏感信息泄露,如用戶密碼、交易金額等。
通過簽名校驗(yàn),可以實(shí)現(xiàn)以下目標(biāo):
- 鑒別調(diào)用者身份:確保請(qǐng)求來自合法的調(diào)用方。
- 驗(yàn)證數(shù)據(jù)完整性:防止參數(shù)在傳輸過程中被篡改。
- 阻止重復(fù)請(qǐng)求:結(jié)合其他機(jī)制,如防重放攻擊,進(jìn)一步保障接口安全。
簽名方案設(shè)計(jì)思路
簽名機(jī)制的核心是對(duì)一組參數(shù)和密鑰進(jìn)行加密,服務(wù)器通過驗(yàn)簽判斷請(qǐng)求的合法性。以下是一個(gè)常見的簽名方案設(shè)計(jì)流程:
- 簽名參數(shù)設(shè)計(jì):
appId:調(diào)用方身份標(biāo)識(shí),用于唯一識(shí)別調(diào)用方。
timestamp:請(qǐng)求時(shí)間戳,用于防止重放攻擊。
nonce:隨機(jī)字符串,增加簽名的唯一性,與timestamp共同防止重放攻擊。
sign:簽名結(jié)果,由其他參數(shù)和密鑰經(jīng)過特定加密算法生成。
- 簽名算法流程:
- 客戶端發(fā)起請(qǐng)求時(shí),將業(yè)務(wù)參數(shù)與公共參數(shù)(
appId、timestamp、nonce)組成有序的Map。 - 將
Map中的參數(shù)按key進(jìn)行排序,拼接成key=value的形式,參數(shù)之間使用特定符號(hào)(如&)連接。 - 在拼接結(jié)果的末尾追加
appSecret(僅服務(wù)端和調(diào)用方知曉的密鑰)。 - 對(duì)拼接后的字符串進(jìn)行
MD5、SHA等加密算法處理,生成最終的sign。 - 服務(wù)器端收到請(qǐng)求后,從請(qǐng)求頭或參數(shù)中讀取
appId,根據(jù)appId獲取對(duì)應(yīng)的appSecret。 - 服務(wù)器按照與客戶端相同的規(guī)則,對(duì)接收到的參數(shù)進(jìn)行排序、拼接、追加
appSecret并加密,生成serverSign。 - 比對(duì)客戶端傳來的
sign和服務(wù)器生成的serverSign,若一致則請(qǐng)求合法,否則拒絕請(qǐng)求。
實(shí)現(xiàn)簽名驗(yàn)證示例
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SignCheck {
boolean required() default true;
}public class SignCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
returntrue;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
SignCheck signCheck = handlerMethod.getMethodAnnotation(SignCheck.class);
if (signCheck == null ||!signCheck.required()) {
returntrue;
}
String appId = request.getHeader("appId");
String timestamp = request.getHeader("timestamp");
String nonce = request.getHeader("nonce");
String sign = request.getHeader("sign");
if (appId == null || timestamp == null || nonce == null || sign == null) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
returnfalse;
}
// 獲取請(qǐng)求參數(shù)
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, String> paramMap = new TreeMap<>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
paramMap.put(entry.getKey(), String.join(",", entry.getValue()));
}
// 拼接參數(shù)
StringBuilder paramBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
paramBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
paramBuilder.append("appSecret=").append(getAppSecret(appId)); // 根據(jù)appId獲取對(duì)應(yīng)的appSecret
// 計(jì)算簽名
String serverSign = calculateSign(paramBuilder.toString());
if (!sign.equals(serverSign)) {
response.setStatus(HttpStatus.FORBIDDEN.value());
returnfalse;
}
returntrue;
}
private String calculateSign(String paramStr) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(paramStr.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private String getAppSecret(String appId) {
// 實(shí)際應(yīng)用中,應(yīng)從數(shù)據(jù)庫(kù)或配置文件中獲取對(duì)應(yīng)的appSecret
// 這里簡(jiǎn)單示例,返回固定值
return"your_secret_key";
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 處理后邏輯,可空
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 完成后邏輯,可空
}
}總結(jié)
- 提供接口文檔和簽名規(guī)則:服務(wù)提供方編寫詳細(xì)的接口文檔,明確接口的功能、請(qǐng)求參數(shù)、響應(yīng)格式以及簽名規(guī)則,包括所需的公共參數(shù)(
appId、timestamp、nonce)、簽名算法、appSecret的獲取方式等,提供給調(diào)用方。 - 調(diào)用方實(shí)現(xiàn)簽名邏輯:調(diào)用方的后端開發(fā)人員根據(jù)接口文檔和簽名規(guī)則,在其代碼中實(shí)現(xiàn)簽名生成邏輯。在每次調(diào)用接口前,按照規(guī)則生成簽名,并將
appId、timestamp、nonce和sign等參數(shù)添加到請(qǐng)求中。 - 前端調(diào)用后端并發(fā)起請(qǐng)求:調(diào)用方的前端頁(yè)面通過調(diào)用自家后端接口,由后端代為簽名并向服務(wù)提供方的接口發(fā)起請(qǐng)求。
- 服務(wù)提供方驗(yàn)簽并返回結(jié)果:服務(wù)提供方的服務(wù)器接收到請(qǐng)求后,首先進(jìn)行簽名驗(yàn)證。如果簽名驗(yàn)證通過,則處理業(yè)務(wù)邏輯,并返回相應(yīng)的結(jié)果給調(diào)用方;如果簽名驗(yàn)證失敗或請(qǐng)求參數(shù)不合法,返回錯(cuò)誤信息給調(diào)用方。


































