面試官:你講下介面防重放如何處理?

程序员博博發表於2024-06-10

前言

我們的API介面都是提供給第三方服務/客戶端呼叫,所有請求地址以及請求引數都是暴露給使用者的。

我們每次請求一個HTTP請求,使用者都可以透過F12,或者抓包工具fd看到請求的URL連結,然後copy出來。這樣是非常不安全的,有人可能會惡意的刷我們的介面,那這時該怎麼辦呢?防重放攻擊就出來了。

什麼是防重放攻擊

我們以掘金文章點贊為例。當我點贊之後,H5會傳送一個請求給到掘金後端伺服器,我可以透過f12看到完整的請求引數,包括url,param等等,然後我可以透過copy把這個請求給copy出來,那麼我就可以做到一個放重放攻擊了。

具體如下。我們可以看到,服務端返回的是重複點贊,也就是掘金並沒有做我們所謂世俗意義上的放重放攻擊。掘金透過查詢資料庫(推測item_id是唯一索引值),來判斷是否已經點贊然後返回前端邏輯。

那麼什麼是我們理解的放重放呢

簡單來說就是,前端和客戶端約定一個演算法(比如md5),透過加密時間戳+傳入欄位。來起到防止重複請求的目的。
然後這個時間戳可以設定為30秒,60秒過期。

那麼如果30秒,有人不斷刷我們的介面怎麼辦。
我們還可以新加一個欄位為nonceKey,30秒內隨機不重複。這個欄位存放在Redis,並且30秒過期。
如果下一次請求nonceKey還在redis,我就認為是重複請求,拒絕即可。

演算法實現

  1. 首先定義一個全域性攔截器
@Component
public class TokenInterceptor implements HandlerInterceptor {

	@Autowired
	private StringRedisTemplate redisService;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		String timestamp = request.getParameter("timestamp");
		String token = request.getParameter("token");
		if (timestamp == null || token == null) {
			return false;
		}

		TreeMap<String, String> map = new TreeMap<>();
		Enumeration<String> parameterNames = request.getParameterNames();
		while (parameterNames.hasMoreElements()) {
			String str = parameterNames.nextElement();
			if (StringUtils.equals(str, "token")) {
				continue;
			}
			map.put(str, request.getParameter(str));
		}

		return SecretUtils.extractSecret(redisService, timestamp, token, map);
	}
}
  1. 定義具體的演算法實現
public class SecretUtils {

	private static final long NONCE_DURATION = 60 * 1000L;
	private static final String SALT = "salt"; // 注意這塊加鹽

	public static boolean extractSecret(StringRedisTemplate redisService, String timestamp, String token, TreeMap<String, String> map) {
		if (StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(token)) {
			return false;
		}
		long ts = NumberUtils.toLong(timestamp, 0);
		long now = System.currentTimeMillis();
		if ((now - ts) > SecretUtils.NONCE_DURATION || ts > now) {
			return false;
		}

		StringBuilder sb = new StringBuilder();
		map.put(SALT, SALT);
		for (Map.Entry<String, String> entry : map.entrySet()) {
			String key = entry.getKey();
			String value = entry.getValue();
			if (sb.length() > 0) {
				sb.append("&");
			}
			sb.append(key).append("=").append(value);
		}

		String targetToken = DigestUtils.md5DigestAsHex(sb.toString().getBytes());
		if (!token.equals(targetToken)) {
			return false;
		}

		String s = redisService.opsForValue().get(timestamp);
		if (StringUtils.isNotEmpty(s)) {
			return false;
		} else {
			redisService.opsForValue().set(timestamp, timestamp, NONCE_DURATION, TimeUnit.MILLISECONDS);
		}

		return true;
	}

}

前端會透過我們事先約定好的演算法以及方式,將字串從小到大進行排序 + timestamp,然後md5進行加密生成token傳給後端。後端根據演算法+方式來校驗token是否有效。

如果其中有人修改了引數,那麼token就會校驗失敗,直接拒絕即可。如果沒修改引數,timestamp如果大於60s,則認為是防重放攻擊,直接拒絕,如果小於30s,則將nonceKey加入到redis裡面,這裡nonceKey用的是timestamp欄位,如果不存在則第一次請求,如果存在,則直接拒絕即可。

透過這麼簡單的一個演算法,就可以實現防重放攻擊了。

Q&A

Q:客戶端和服務端生成的時間戳不一致怎麼辦

A:客戶端和服務端生成的是時間戳,不是具體的時間,時間戳是指格林威治時間1970年01月01日00時00分00秒(北京時間1970年01月01日08時00分00秒)起至現在的總秒數

Q:HTTPS資料加密是否可以防止重放攻擊

A:不可以。https是在傳輸過程中保證了加密,也就是說如果中間人,獲取到了請求,他是無法解開傳輸的內容的。

舉個最簡單的例子,上課和同學傳紙條的時候,為了不讓中間給遞紙條的人看到或者修改,可以在紙條上寫成只有雙方能看明白密文,這樣遞紙條的過程就安全了,傳紙條過程中的人就看不懂你的內容了。但是如果給你寫紙條的人要搞事情,那就是加密解決不了的了。這時候就需要放重放來解決了。

Q:防重放攻擊是否有用,屬於脫褲子放屁

A:個人感覺有一點點吧。比如防重放攻擊的演算法+加密方式其實大多數用的都是這些,其實攻擊人很容易就能猜到token生成的方式,比如timestamp + 從小到大排序。因此我們加入了salt來混淆視聽,這個salt需要前端、客戶端安全的儲存,不能讓使用者知道,比如js混淆等等。但其實透過抓包,js分析還是很容易能拿到的。但無形中增加了攻擊人的成本,比如網易雲登入的js加密類似。

Q:做了防重放,支付,點贊等是否不需要做冪等了

A:需要。最重要的冪等,一定要用資料庫來實現,比如唯一索引。其他都不可相信。

最後

以我個人的理解。防重放用處不大,其他安全措施,比如非對稱的RSA驗籤更加有效。就算使用者拿到了請求的所有資訊,你的介面也一定要做冪等的,尤其是像支付轉賬等高危操作,冪等才是最有用的防線。而且防重發生成token的演算法,大家都這樣搞,攻擊者怎麼可能不知道呢?這點我不太理解。

現在面試也比較考驗面試官的水平,下篇我會講下最近的一些面試體驗和感受,歡迎大家點贊收藏。

相關文章