切換語言為:簡體

基於 Redis 的 lua 指令碼方式實現同一 ip 多次密碼錯誤鎖定賬號邏輯

  • 爱糖宝
  • 2024-06-03
  • 2093
  • 0
  • 0

不能鎖username,因為如果有人惡意保留破解密碼的話。會導致使用者本人無法登入。 這裏我採用 以ip的方式進行鎖定。利用redis 設定key:ip。value:當前ip嘗試登入的次數

實現邏輯

邏輯簡單,假設限制錯誤次數為5

  1. 使用者登入時。判斷key是否存在。

  2. 如果key不存在,說明第一次登入。判斷密碼是否正確,==如果正確==什麼都不幹直接返回。==如果不正確==,設定key為ip,value為1,並設定過期時間,返回錯誤資訊。

  3. 如果存在key,說明之前嘗試登入過。判斷value是否大於閾值,如果大於閾值重新整理過期 並返回錯誤。如果小於閾值,==還要判斷密碼是否正確==。如果密碼正確,刪除key並返回正確資訊。如果密碼不正確,將key的value值+1並重置過期時間,返回密碼錯誤的資訊。

以上主要注意兩點 一點是有key的情況下密碼正確需要刪除key。 二是有key的情況下,密碼錯誤或者嘗試次數大於閾值再次嘗試登入,需要重新整理過期時間

使用lua指令碼主要保證了對上述邏輯的原子性,因為涉及獲取key的值並判斷,然後將key的值+1 或 刪除key。

實現程式碼

Service類載入時,載入lua指令碼

    private static final DefaultRedisScript<Long> CHECK_LOGIN_SCRIPT;

    // 類載入時 載入lua指令碼
    static {
        CHECK_LOGIN_SCRIPT = new DefaultRedisScript<>();
        CHECK_LOGIN_SCRIPT.setLocation(new ClassPathResource("checkLogin.lua"));
        CHECK_LOGIN_SCRIPT.setResultType(Long.class);
    }

具體校驗方法,主要邏輯呼叫了lua指令碼

    private void checkLoginInfo(UserLoginContext userLoginContext) {
        String username = userLoginContext.getUsername();
        String password = userLoginContext.getPassword();

        //根據使用者名稱查實體 從資料庫
        RPanUser entity = getRPanUserByUsername(username);
        if (Objects.isNull(entity)){
            throw new RPanBusinessException("使用者名稱不存在");
        }

        String salt = entity.getSalt();             //獲取鹽值
        String encPassword = PasswordUtil.encryptPassword(salt, password) ;  //將前端傳 的密碼加密
        String dbPassword = entity.getPassword();   //獲取資料庫的密碼
        // encPassword 表示前端傳過來的 加密後的密碼
        // dbPassword  表示資料庫中的   加密後的密碼
        // TODO 呼叫lua指令碼判斷 登入失敗 登入成功 及鎖定ip
        List<String> keys = new ArrayList<>();
        String ipAddress = HttpLogEntityBuilder.getIpAddress(userLoginContext.getRequest());    //獲取請求ip
        // key設定為 ip+username,進行鎖定
        keys.add(UserConstants.CHECK_LOGIN_PREFIX + ipAddress + "_" + username);
        // 執行lua指令碼
        Long result = (Long)redisTemplate.execute(CHECK_LOGIN_SCRIPT, keys, encPassword, dbPassword, UserConstants.CHECK_LOGIN_THRESHOLD, UserConstants.CHECK_LOGIN_EXPIRE);

        if (result == 0){
            throw new RPanBusinessException("使用者名稱或密碼不正確");
        }
        if (result == -1){
            throw new RPanBusinessException("輸入密碼錯誤達到"+UserConstants.CHECK_LOGIN_THRESHOLD+"次,請1分鐘後嘗試");
        }
//        if (!Objects.equals(encPassword, dbPassword)) {
//            throw new RPanBusinessException("密碼資訊不正確");
//        }
        //填入實體資訊
        userLoginContext.setEntity(entity);
    }

lua指令碼,放在resources目錄下

-- 判斷使用者登入的lua指令碼

local key1 = KEYS[1]
local password1 = ARGV[1]
local password2 = ARGV[2]
-- 閾值
local threshold = ARGV[3]
-- 過期時間
local expiretime = ARGV[4]

-- 判斷key是否存在
local value = redis.call('get', key1)

-- 有key
if value then
    if(value >= threshold) then
        redis.call('expire', key1, expiretime)  -- 重新整理過期時間
        return -1                               -- 輸入密碼錯誤達到threshold次,請1分鐘後嘗試
    end

    if(password1 == password2) then -- 校驗正確,刪除key並返回1表示透過
        redis.call('del', key1)
        return 1
    else
        redis.call('INCR', key1)               -- key的值+1
        redis.call('expire', key1, expiretime) -- 重新整理過期時間
        return 0                                -- 使用者名稱或密碼不正確
    end
end

-- 沒有key
if(password1 == password2) then
    return 1
else
    redis.call('setex', key1, expiretime, 1) -- setex key [過期時間] 1
    return 0   -- 使用者名稱或密碼不正確
end

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.