不能鎖username,因為如果有人惡意保留破解密碼的話。會導致使用者本人無法登入。 這裏我採用 以ip的方式進行鎖定。利用redis 設定key:ip。value:當前ip嘗試登入的次數
實現邏輯
邏輯簡單,假設限制錯誤次數為5
使用者登入時。判斷key是否存在。
如果key不存在,說明第一次登入。判斷密碼是否正確,==如果正確==什麼都不幹直接返回。==如果不正確==,設定key為ip,value為1,並設定過期時間,返回錯誤資訊。
如果存在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