臭名昭著的手機驗證碼功能是如何實現的

  • 2020 年 4 月 17 日
  • 筆記

前言

現在基本上各種手機APP註冊都會用到手機驗證碼,包括一些PC端網站也會使用手機號作為唯一標識驗證!

恰巧,小明的老闆,讓其開發一個用戶註冊的功能,並且強制用戶註冊綁定手機,美其名曰為了提升安全性,呵呵噠,就是為了多擼一點用戶資訊。

案例

一般來說,發送手機驗證碼不能過於頻繁,前端發送按鈕點擊後一般會有一個60秒倒計時的功能。也就是說,如果用戶點擊發送一直沒有收到驗證碼,只能60秒之後才可以進行重發。

那麼問題來了,如果用戶繞過前端,直接向後台API發送簡訊請求,然後寫個無限循環腳本,相信不久你的簡訊賬戶就會發來預警提示簡訊(一般來說大的簡訊商都有預警設置功能)。

其實很簡單,你只需要F12,查看發送請求就可以查找出後台請求地址,然後你可以在控制台輸入相關JS程式碼,執行個十萬遍,是不是很爽?

這裡以七牛云為測試案例,打開註冊頁面,F12進入調試模式,輸入手機號,手動點擊發送,獲取其簡訊發送後台請求地址。下面是七牛雲的一個簡訊發送請求,擼主測試了一下,顯然沒有達到擼主的預期,畢竟是大廠,防禦措施還是做的很牛逼的。

以下是JS腳本,複製粘貼到控制台回車就可以執行:

var data = {"operation":1,"is_voice":false,"mobile_number":"17762018888","captcha_type":2};
for (var i = 0; i < 10; i++) {
    $.ajax({
        type: 'POST',
		contentType: 'application/json;charset=UTF-8',
        data:JSON.stringify(data),
        url: '//portal.qiniu.com/api/gaea/verification/sms/send',
        success: function(data) {
            console.log(data)
        }
    });
}

控制台返回以下資訊,前三次請求成功,後面的就出現了驗證碼校驗並進行了限流操作。

{"code":200,"message":""}
{"code":200,"message":""}
{"code":200,"message":""}
{"code": 7209,"message":"captcha required"}
{"code": 7209,"message":"captcha required"}
{"code": 429,"message":"too many requests"}
{"code": 429,"message":"too many requests"}
{"code": 429,"message":"too many requests"}
{"code": 429,"message":"too many requests"}
{"code": 7209,"message":"captcha required"}

擼主嘗試刷新頁面,隨便輸了一個手機號,再次點擊發送,提示用戶輸入驗證碼,顯然是加強了防備,觸發了惡意請求認證攔截機制。

安全機制

對於開發者來說,他們不僅要考慮用戶正常獲取驗證碼的體驗還要考慮簡訊介面的安全性,擼主總結了以下幾點,希望對大家有所幫助。

  • 後台請求限流,對單位時間內發送頻率做限制。
  • 驗證碼機制,切記不要一開始就限制驗證碼,體驗及其不友好,觸發限流以後開啟驗證碼校驗。
  • 監控日發送簡訊數量,觸發一定的閾值做相應的處理,根據實際業務需求。
  • 驗證碼存儲一定要保證key為手機號,切記不要以其它標識作為key,比如sessionId
  • 一定要設置驗證碼失效時間,比如五分鐘,或者更短。
  • 驗證碼盡量保證短小精悍,四到六位即可。
  • 如果後台不做限制,切記前台一定要做個倒計時的限制,至少過濾一部分小白用戶。

程式碼案例

給小夥伴分享一個簡單的驗證碼生成、存儲、失效程式碼案例:

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class Mobile {
    /**
     * 測試方便,這裡設置了3秒失效
     */
    private static LoadingCache<String, String> caches = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(3, TimeUnit.SECONDS)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String mobile) {
                    return "";
                }
            });

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Integer code = (int)((Math.random()*9+1)*100000);
        caches.put("17762018888",code.toString());
        System.out.println(caches.get("17762018888"));
        Thread.sleep(4000);
        System.out.println("是不是沒了:"+caches.get("17762018888"));
    }
}

小結

重要的功能必須進行前後端校驗,必要的時候一定要做好限流、黑名單等騷操作!!!