后臺(tái)登陸防刷、防爆破以及正常的登錄校驗(yàn)


背景

前幾天項(xiàng)目上需要對(duì)一個(gè)正常登陸接口,以及忘記密碼的接口進(jìn)行防爆破處理,這里我用nginx,redis,以及前端的一些簡(jiǎn)單的圖形拖動(dòng)來(lái)做一個(gè)簡(jiǎn)單的安全機(jī)制,可能有不完善的地方,大家可以提出來(lái)意見。

技術(shù)分析

其實(shí)一個(gè)接口是無(wú)法完全避免接口爆破的,區(qū)分人和機(jī)器或許可以使用谷歌的圖片驗(yàn)證機(jī)制,但是我們一般簡(jiǎn)單項(xiàng)目沒必要做那么復(fù)雜的,只需要確保不正常的訪問(wèn)頻率不會(huì)爆破出我們的用戶信息,以及讓我們機(jī)器的處理流量保存在可控范圍即可。

實(shí)現(xiàn)的效果有下面這幾點(diǎn):

驗(yàn)證碼只能60s獲取一次 并且3小時(shí)內(nèi)只能獲取三次,超過(guò)次數(shù)提升獲取頻繁,稍后再試。

正常登錄1小時(shí)內(nèi)失敗6次賬號(hào)自動(dòng)鎖定,1小時(shí)之后自動(dòng)解鎖。

獲取驗(yàn)證碼無(wú)論輸入的賬號(hào)存在不存在均顯示發(fā)送成功,但是實(shí)際不存在的賬號(hào)不會(huì)正常發(fā)送。

4.登錄失敗,賬號(hào)不存在密碼錯(cuò)誤不再提示賬號(hào)不存在等等,而是統(tǒng)一顯示賬號(hào)或密碼錯(cuò)誤。5.忘記密碼前端部分增加滑動(dòng)校驗(yàn),60倒計(jì)時(shí)無(wú)法點(diǎn)擊發(fā)送驗(yàn)證碼。前后端共同校驗(yàn)。6.技術(shù)限制系統(tǒng)此接口的訪問(wèn)頻率。

前端部分



前端部分可以在這個(gè)地址看看這幾個(gè)簡(jiǎn)單的組件,這次我們就使用最簡(jiǎn)單的,滑動(dòng)拖動(dòng)即可。

<drag-verify

? ? ? ? ? ? ? ref="dragVerify"

? ? ? ? ? ? ? :width="width"

? ? ? ? ? ? ? :height="height"

? ? ? ? ? ? ? text="請(qǐng)按住滑塊拖動(dòng)"

? ? ? ? ? ? ? successText="驗(yàn)證通過(guò)"

? ? ? ? ? ? ? :isPassing.sync="isPassing"

? ? ? ? ? ? ? background="#ccc"

? ? ? ? ? ? ? completedBg="rgb(105, 231, 251)"

? ? ? ? ? ? ? handlerIcon="el-icon-d-arrow-right"

? ? ? ? ? ? ? successIcon="el-icon-circle-check"

? ? ? ? ? ? ? @passcallback="passcallback"

? ? ? ? ? >

? ? ? ? ? </drag-verify>


用戶滑動(dòng)之后需要加上60s倒計(jì)時(shí),這塊我們使用定時(shí)器實(shí)現(xiàn)即可,以及郵箱和手機(jī)號(hào)的正確性校驗(yàn),不正確則彈窗提示。

this.countDown = 60;

? ? ? timer = setInterval(() => {

? ? ? ? if (this.countDown - 1 >= 0) {

? ? ? ? ? this.countDown -= 1;

? ? ? ? } else {

? ? ? ? ? clearInterval(timer);

? ? ? ? ? timer = null;

? ? ? ? }

? ? ? }, 1000);


<el-button disabled type="text" v-show="time > 0">

{{ time > 0 ? `${time}` : "" }} s之后重試</el-button>

驗(yàn)證郵箱手機(jī)號(hào)可以使用正則校驗(yàn)進(jìn)行。

mobileReg = /^1\d{10}$/;

? ? ? emailReg = /^([A-Za-z0-9_\-\.\u4e00-\u9fa5])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,8})$/;

前端大體思路就是,進(jìn)行滑塊驗(yàn)證,拖到右邊之后,60s之內(nèi)無(wú)法操作,60s到期之后自動(dòng)復(fù)原,

顯示倒計(jì)時(shí)時(shí)間。這個(gè)只能防止用戶在頁(yè)面上多次點(diǎn)擊,造成一個(gè)驗(yàn)證的假象,如果直接對(duì)后端接口爆破,則無(wú)法避免。

后端

這是大概的流程圖,圖中還有些細(xì)節(jié)問(wèn)題下面慢慢講解。


這塊本來(lái)我想用java或者kotlin寫,但是歷史項(xiàng)目用go寫的,重寫的話還有其他一些改動(dòng),所以繼續(xù)使用golang完成這部分邏輯。

先定義一個(gè)結(jié)構(gòu)體,然后我們來(lái)分析下需要哪些字段來(lái)實(shí)現(xiàn)我們的業(yè)務(wù)。

我們需要一個(gè)首次登陸時(shí)間,最后一次登陸時(shí)間,和總的登錄次數(shù)足以判斷:

驗(yàn)證碼間隔

登陸失敗次數(shù)

距離第一次登陸失敗次數(shù)間隔

type CommonLogin struct {

? ? CreateTime time.Time

? ? LastTime? time.Time

? ? Times? ? ? uint8

}

正常登陸的前置校驗(yàn)

// 登錄的前置校驗(yàn)

func beforeCommonLoginValid(key string, r *redis.Client, field string) (bool, error) {

? ? // redis中是否存在賬號(hào)

? ? result, err := r.HExists(field, key).Result()

? ? if err != nil {

? ? ? ? ? ? fmt.Printf("從redis中獲取用戶賬戶失敗,賬戶為: %s", key)

? ? ? ? ? ? return false, err

? ? }

? ? if result {

? ? ? ? ? ? login := &CommonLogin{}

? ? ? ? ? ? // 存在賬號(hào) 說(shuō)明之前登錄失敗過(guò) 且自從上次失敗未登錄成功過(guò)

? ? ? ? ? ? commonLogin, err := r.HGet(field, key).Result()

? ? ? ? ? ? if err != nil {

? ? ? ? ? ? ? ? ? ? return false, err

? ? ? ? ? ? }

? ? ? ? ? ? json.Unmarshal([]byte(commonLogin), login)

? ? ? ? ? ? if login.Times < 6 {

? ? ? ? ? ? ? ? ? ? return true, nil

? ? ? ? ? ? }

? ? ? ? ? ? // 是否在1小時(shí)內(nèi)失敗了6次

? ? ? ? ? ? if login.Times >= 6 {

? ? ? ? ? ? ? ? ? ? // 否

? ? ? ? ? ? ? ? ? ? if time.Now().Sub(login.CreateTime) > time.Hour*1 {

? ? ? ? ? ? ? ? ? ? ? ? ? ? // 連續(xù)輸錯(cuò)6次時(shí)長(zhǎng)大于1小時(shí) 解鎖

? ? ? ? ? ? ? ? ? ? ? ? ? ? r.HDel(field, key)

? ? ? ? ? ? ? ? ? ? ? ? ? ? return true, nil

? ? ? ? ? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? ? ? ? ? ? fmt.Printf("用戶%s于1小時(shí)之內(nèi)連續(xù)登錄失敗6次,賬號(hào)鎖定,1小時(shí)后重試。", key)

? ? ? ? ? ? ? ? ? ? ? ? ? ? return false, nil

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? }

? ? // redis中不存在重試記錄

? ? return true, nil

}

在所有的登錄判斷的出口,調(diào)用此方法即可,例如用戶名密碼錯(cuò)誤,acl校驗(yàn)未通過(guò)等等。

忘記密碼的登錄前置校驗(yàn)

其實(shí)原理差不多,唯一的區(qū)別就是多了一個(gè)獲取驗(yàn)證碼時(shí)間間隔校驗(yàn)。

func beforeForgotPasswordValid(key string, r *redis.Client, field string) (bool, error) {

? ? // redis中是否存在賬號(hào)

? ? result, err := r.HExists(field, key).Result()

? ? if err != nil {

? ? ? ? ? ? fmt.Printf("從redis中獲取用戶賬戶失敗,賬戶為: %s", key)

? ? ? ? ? ? return false, err

? ? }

? ? login := &CommonLogin{}

? ? // 賬號(hào)存在

? ? if result {

? ? ? ? ? ? commonLogin, err := r.HGet(field, key).Result()

? ? ? ? ? ? if err != nil {

? ? ? ? ? ? ? ? ? ? return false, err

? ? ? ? ? ? }

? ? ? ? ? ? json.Unmarshal([]byte(commonLogin), login)

? ? ? ? ? ? // 獲取驗(yàn)證碼間隔時(shí)長(zhǎng)不能小于60s

? ? ? ? ? ? if time.Now().Sub(login.LastTime) < time.Second*60 {

? ? ? ? ? ? ? ? ? ? fmt.Printf("用戶獲取驗(yàn)證碼間隔小于60s")

? ? ? ? ? ? ? ? ? ? return false, nil

? ? ? ? ? ? }

? ? ? ? ? ? if login.Times < 3 {

? ? ? ? ? ? ? ? ? ? return true, nil

? ? ? ? ? ? }

? ? ? ? ? ? // 是否在1小時(shí)內(nèi)獲取了3次

? ? ? ? ? ? if login.Times >= 3 {

? ? ? ? ? ? ? ? ? ? // 否

? ? ? ? ? ? ? ? ? ? if time.Now().Sub(login.CreateTime) > time.Hour*3 {

? ? ? ? ? ? ? ? ? ? ? ? ? ? // 連續(xù)輸錯(cuò)6次時(shí)長(zhǎng)大于1小時(shí) 解鎖

? ? ? ? ? ? ? ? ? ? ? ? ? ? r.HDel(field, key)

? ? ? ? ? ? ? ? ? ? ? ? ? ? return true, nil

? ? ? ? ? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? ? ? ? ? ? fmt.Printf("用戶%s于3小時(shí)之內(nèi)連續(xù)獲取驗(yàn)證碼3次,賬號(hào)鎖定,3小時(shí)后重試。", key)

? ? ? ? ? ? ? ? ? ? ? ? ? ? return false, nil

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? }

? ? return true, nil

}

忘記密碼的后置校驗(yàn)

// 更新獲取驗(yàn)證碼的時(shí)間

func afterForgotPasswordValid(key string, r *redis.Client, field string) {

? ? login := &CommonLogin{}

? ? commonLogin, _ := r.HGet(field, key).Result()

? ? json.Unmarshal([]byte(commonLogin), login)

? ? // 驗(yàn)證碼發(fā)送成功

? ? result, _ := r.HExists(field, key).Result()

? ? if result {

? ? ? ? ? ? login.Times = login.Times + 1

? ? ? ? ? ? login.LastTime = time.Now()

? ? ? ? ? ? data, _ := json.Marshal(login)

? ? ? ? ? ? r.HSet(field, key, data)

? ? } else {

? ? ? ? ? ? login.Times = 1

? ? ? ? ? ? login.LastTime = time.Now()

? ? ? ? ? ? login.CreateTime = login.LastTime

? ? ? ? ? ? data, _ := json.Marshal(login)

? ? ? ? ? ? r.HSet(field, key, data)

? ? }

}

使用nginx進(jìn)行接口訪問(wèn)頻率限制

nginx是一個(gè)非常強(qiáng)大的中間價(jià),在安全方面,我們可以用它來(lái)限制來(lái)自于同一機(jī)器的訪問(wèn)頻率,可以做黑名單功能等等,當(dāng)然有人會(huì)說(shuō)ip代{過(guò)}{濾}理池之類的,我們此次演示的只是簡(jiǎn)單demo,惡意攻擊當(dāng)然需要專業(yè)防護(hù)了。

具體google一下,看這兩篇官方文檔。

https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http

https://www.nginx.com/blog/rate-limiting-nginx/

具體的配置其實(shí)很簡(jiǎn)單了。

限制遠(yuǎn)程同ip訪問(wèn)頻率。

limit_req_zone$binary_remote_addrzone=perip:10mrate=1r/s;

解釋下這段配置的參數(shù):

$binary_remote_addr 表示通過(guò)remote

addr這個(gè)標(biāo)識(shí)來(lái)做限制,“binary”的目的是縮寫內(nèi)存占用量,是限制同一客戶端ip地址

zone=one:10m表示生成一個(gè)大小為10M,名字為one的內(nèi)存區(qū)域,用來(lái)存儲(chǔ)訪問(wèn)的頻次信息

rate=1r/s表示允許相同標(biāo)識(shí)的客戶端的訪問(wèn)頻次,這里限制的是每秒1次,還可以有比如30r/m的

location ^~ /api/xxx {

? ? ? ? limit_req zone=perip nodelay;

? ? ? ? limit_req_status 503;

? ? ? ? proxy_pass http://正確地址;

? ? }

上面配置意思就是超過(guò)頻率返回503,服務(wù)不可用。

使用jmeter進(jìn)行壓力測(cè)試:1s 10個(gè)請(qǐng)求,我們預(yù)期只有1個(gè)請(qǐng)求成功,其他的返回503.



核心邏輯其實(shí)就是上面這些,源碼不貼出來(lái)了,有不懂的再討論吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容