成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Java 中實現用戶登錄次數限制的多種方案

開發 前端
如果你是單機應用,ConcurrentHashMap? 和 ScheduledExecutorService? 可能是個不錯的選擇;如果是分布式系統,使用 Redis? 方案會更有優勢;而對于 Spring? 框架的項目,Spring Security 方案會更貼合你的需求。?

前言

在開發應用程序時,為了保障系統的安全性,我們常常需要對用戶的登錄行為進行限制,比如規定用戶在 5 分鐘內最多允許嘗試登錄 3 次,如果超過次數,就鎖定當前用戶。今天我們就來探討幾種在 Java 中實現這一功能的方案,讓我們的系統更加安全可靠。

使用 HashMap 和 Timer 實現

首先,我們創建一個 HashMap 來存儲用戶登錄失敗的信息。這個 HashMap 中的鍵是用戶名,而值是一個自定義的 LoginAttempt 對象,它包含登錄失敗次數和最近一次失敗時間。當用戶進行登錄操作時,我們會檢查 HashMap 中是否存在該用戶的記錄。若存在,會查看是否超過 5 分鐘,如果超過,我們將重置失敗次數;如果未超過且次數已達 3 次,將拒絕登錄。同時,我們使用 Timer 來清理過期的記錄。

示例

public class LoginAttempt {
    int attempts;
    long lastAttemptTime;

    public LoginAttempt() {
        this.attempts = 0;
        this.lastAttemptTime = System.currentTimeMillis();
    }
}

public class LoginService {
    private static final Map<String, LoginAttempt> loginAttempts = new HashMap<>();
    private static final long LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
    private static final int MAX_ATTEMPTS = 3;

    public static boolean isLoginAllowed(String username) {
        LoginAttempt attempt = loginAttempts.get(username);
        if (attempt == null) {
            return true;
        }
        long currentTime = System.currentTimeMillis();
        if (currentTime - attempt.lastAttemptTime > LOCKOUT_DURATION) {
            loginAttempts.remove(username);
            return true;
        }
        if (attempt.attempts >= MAX_ATTEMPTS) {
            return false;
        }
        return true;
    }

    public static void recordFailedLogin(String username) {
        LoginAttempt attempt = loginAttempts.get(username);
        if (attempt == null) {
            attempt = new LoginAttempt();
            loginAttempts.put(username, attempt);
        }
        attempt.attempts++;
        attempt.lastAttemptTime = System.currentTimeMillis();
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                long currentTime = System.currentTimeMillis();
                loginAttempts.entrySet().removeIf(entry -> currentTime - entry.getValue().lastAttemptTime > LOCKOUT_DURATION);
            }
        }, LOCKOUT_DURATION, LOCKOUT_DURATION);

        // 模擬登錄嘗試
        String username = "testUser";
        for (int i = 0; i < 5; i++) {
            if (isLoginAllowed(username)) {
                System.out.println("Login allowed");
                // 模擬登錄成功,重置嘗試次數
                loginAttempts.remove(username);
            } else {
                System.out.println("Login not allowed, user locked");
            }
            recordFailedLogin(username);
        }
    }
}

使用 ConcurrentHashMap 和 ScheduledExecutorService 實現

這里我們使用 ConcurrentHashMap 來存儲用戶登錄失敗信息,它支持并發訪問。并且使用 ScheduledExecutorService 來進行定時清理過期記錄,避免 Timer 的一些潛在問題,如可能導致的內存泄漏,更適合高并發場景。

示例

public class LoginAttempt {
    int attempts;
    long lastAttemptTime;

    public LoginAttempt() {
        this.attempts = 0;
        this.lastAttemptTime = System.currentTimeMillis();
    }
}

public class LoginService {
    private static final ConcurrentHashMap<String, LoginAttempt> loginAttempts = new ConcurrentHashMap<>();
    private static final long LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
    private static final int MAX_ATTEMPTS = 3;
    private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    static {
        executorService.scheduleAtFixedRate(() -> {
            long currentTime = System.currentTimeMillis();
            loginAttempts.entrySet().removeIf(entry -> currentTime - entry.getValue().lastAttemptTime > LOCKOUT_DURATION);
        }, LOCKOUT_DURATION, LOCKOUT_DURATION, TimeUnit.MILLISECONDS);
    }

    public static boolean isLoginAllowed(String username) {
        LoginAttempt attempt = loginAttempts.get(username);
        if (attempt == null) {
            return true;
        }
        long currentTime = System.currentTimeMillis();
        if (currentTime - attempt.lastAttemptTime > LOCKOUT_DURATION) {
            loginAttempts.remove(username);
            return true;
        }
        if (attempt.attempts >= MAX_ATTEMPTS) {
            return false;
        }
        return true;
    }

    public static void recordFailedLogin(String username) {
        loginAttempts.compute(username, (key, value) -> {
            if (value == null) {
                return new LoginAttempt();
            }
            value.attempts++;
            value.lastAttemptTime = System.currentTimeMillis();
            return value;
        });
    }

    public static void main(String[] args) {
        // 模擬登錄嘗試
        String username = "testUser";
        for (int i = 0; i < 5; i++) {
            if (isLoginAllowed(username)) {
                System.out.println("Login allowed");
                // 模擬登錄成功,重置嘗試次數
                loginAttempts.remove(username);
            } else {
                System.out.println("Login not allowed, user locked");
            }
            recordFailedLogin(username);
        }
    }
}

使用 Redis 存儲登錄嘗試信息

借助 Redis 的鍵值存儲,我們將用戶的登錄失敗次數和最近一次失敗時間存儲起來,并利用 Redis 的 TTL(Time To Live)功能自動清理過期記錄。在用戶登錄時,從 Redis 獲取信息,據此判斷是否允許登錄。

示例

public class LoginService {
    private static final Jedis jedis = new Jedis("localhost", 6379);
    private static final long LOCKOUT_DURATION = 5 * 60; // 5 minutes in seconds
    private static final int MAX_ATTEMPTS = 3;

    public static boolean isLoginAllowed(String username) {
        String attemptsKey = "login_attempts:" + username;
        String attemptsStr = jedis.get(attemptsKey);
        if (attemptsStr == null) {
            return true;
        }
        String[] parts = attemptsStr.split(":");
        int attempts = Integer.parseInt(parts[0]);
        long lastAttemptTime = Long.parseLong(parts[1]);
        long currentTime = System.currentTimeMillis() / 1000;
        if (currentTime - lastAttemptTime > LOCKOUT_DURATION) {
            jedis.del(attemptsKey);
            return true;
        }
        if (attempts >= MAX_ATTEMPTS) {
            return false;
        }
        return true;
    }

    public static void recordFailedLogin(String username) {
        String attemptsKey = "login_attempts:" + username;
        String attemptsStr = jedis.get(attemptsKey);
        long currentTime = System.currentTimeMillis() / 1000;
        if (attemptsStr == null) {
            jedis.setex(attemptsKey, (int) LOCKOUT_DURATION, "1:" + currentTime);
        } else {
            String[] parts = attemptsStr.split(":");
            int attempts = Integer.parseInt(parts[0]);
            jedis.setex(attemptsKey, (int) LOCKOUT_DURATION, (attempts + 1) + ":" + currentTime);
        }
    }

    public static void main(String[] args) {
        // 模擬登錄嘗試
        String username = "testUser";
        for (int i = 0; i < 5; i++) {
            if (isLoginAllowed(username)) {
                System.out.println("Login allowed");
                // 模擬登錄成功,刪除嘗試記錄
                jedis.del("login_attempts:" + username);
            } else {
                System.out.println("Login not allowed, user locked");
            }
            recordFailedLogin(username);
        }
    }
}

補充-滑動窗口

構造一個滑動窗口,窗口大小限制5分鐘,然后限流次數設置為3次即可實現這個功能了。而滑動窗口我們可以借助Redis來實現。

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private String key;
    private int limit = 3; //限制請求次數最大3次
    private int lockTime;  // 鎖定用戶的時間,單位:秒

    public SlidingWindowRateLimiter(Jedis jedis, String key, int limit, int lockTime) {
        this.jedis = jedis;
        this.key = key;
        this.limit = limit;
        this.lockTime = lockTime;  // 鎖定時間    
    }

    public boolean allowRequest() {
        // 當前時間戳,單位:毫秒        
        long currentTime = System.currentTimeMillis();
        // 鎖定鍵的名稱(鎖定的用戶)
        String lockKey = "lock:" + key;
        // 檢查用戶是否已被鎖定
        if (jedis.exists(lockKey)) {
            returnfalse;  // 用戶已被鎖定,返回 false        
        }
        // 使用Lua腳本來確保原子性操作
        String luaScript = "local window_start = ARGV[1] - 300000\n" + // 計算5分鐘的起始時間
                "redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" +  // 清理過期的請求
                "local current_requests = redis.call('ZCARD', KEYS[1])\n" +  // 獲取當前請求次數
                "if current_requests < tonumber(ARGV[2]) then\n" +  // 如果請求次數小于限制
                "    redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" +  // 添加當前請求時間
                "    return 1\n" +  // 允許請求
                "else\n" +
                "    redis.call('SET', 'lock:'..KEYS[1], 1, 'EX', tonumber(ARGV[3]))\n" +  // 鎖定用戶
                "    return 0\n" +  // 拒絕請求
                "end";
        // 調用 Lua 腳本進行原子操作
        Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(limit), String.valueOf(lockTime));
        // 返回操作結果
        return (Long) result == 1;
    }
}

使用 Spring Security 實現

我們還可以使用 Spring Security 的相關組件來實現。利用 AuthenticationFailureHandler 記錄登錄失敗信息,在 UserDetailsService 的 loadUserByUsername 方法中檢查用戶是否被鎖定,使用 UserDetails 的 isAccountNonLocked 屬性表示用戶是否被鎖定。

示例

public class CustomAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;
    private Map<String, LoginAttempt> loginAttempts = new HashMap<>();
    private static final long LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
    private static final int MAX_ATTEMPTS = 3;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = new BCryptPasswordEncoder();
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        UserDetails userDetails;
        try {
            userDetails = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            throw new BadCredentialsException("Invalid username or password");
        }
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            recordFailedLogin(username);
            throw new BadCredentialsException("Invalid username or password");
        }
        if (!isLoginAllowed(username)) {
            throw new BadCredentialsException("User is locked");
        }
        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }

    private boolean isLoginAllowed(String username) {
        LoginAttempt attempt = loginAttempts.get(username);
        if (attempt == null) {
            return true;
        }
        long currentTime = System.currentTimeMillis();
        if (currentTime - attempt.lastAttemptTime > LOCKOUT_DURATION) {
            loginAttempts.remove(username);
            return true;
        }
        if (attempt.attempts >= MAX_ATTEMPTS) {
            return false;
        }
        return true;
    }

    private void recordFailedLogin(String username) {
        LoginAttempt attempt = loginAttempts.get(username);
        if (attempt == null) {
            attempt = new LoginAttempt();
            loginAttempts.put(username, attempt);
        }
        attempt.attempts++;
        attempt.lastAttemptTime = System.currentTimeMillis();
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

    public static class LoginAttempt {
        int attempts;
        long lastAttemptTime;

        public LoginAttempt() {
            this.attempts = 0;
            this.lastAttemptTime = System.currentTimeMillis();
        }
    }
}

最后

如果你是單機應用,ConcurrentHashMap 和 ScheduledExecutorService 可能是個不錯的選擇;如果是分布式系統,使用 Redis 方案會更有優勢;而對于 Spring 框架的項目,Spring Security 方案會更貼合你的需求。

責任編輯:武曉燕 來源: 一安未來
相關推薦

2025-06-05 05:22:00

2018-04-02 10:16:00

bug代碼安卓

2009-12-23 10:46:38

WPF實現用戶界面

2009-12-30 09:45:52

Silverlight

2010-01-28 10:00:54

linux用戶注銷logout

2012-05-04 09:28:49

Linux

2010-08-04 10:48:17

路由器

2021-08-05 10:40:37

加密方案Spring

2024-09-22 10:46:33

數據飛輪算法

2016-10-24 23:18:55

數據分析漏斗留存率

2009-09-07 09:20:34

2011-03-24 08:56:23

escalationsNagios報警

2024-04-08 14:10:06

2016-05-17 10:03:39

用戶體驗運維可度量

2018-05-30 10:22:47

電商平臺

2019-08-22 15:42:03

2025-03-28 04:10:00

2024-01-10 08:26:16

用戶注冊用戶重復數據

2010-06-18 13:52:24

SQL Server查
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 精品久久久久一区二区国产 | 国产免费a | 亚洲97 | 欧美一级片在线看 | 视频一区中文字幕 | 国产精品久久久久久久久久久新郎 | 日韩成人在线视频 | 日本不卡一区二区三区 | 99re热精品视频 | 亚洲精品在线免费播放 | 在线观看国产wwwa级羞羞视频 | 蜜臀久久99精品久久久久久宅男 | 久久精品国产清自在天天线 | 久久99视频| 北条麻妃一区二区三区在线视频 | 国产一区影院 | 成人av一区二区亚洲精 | 538在线精品| 亚洲在线一区二区 | 日韩高清中文字幕 | 九九九久久国产免费 | 午夜精品导航 | 日日夜夜草 | 91热爆在线观看 | 围产精品久久久久久久 | 国产一二三区免费视频 | 亚洲一区二区三区高清 | 国产成人小视频 | 中文字幕在线播放第一页 | 欧美男人天堂 | 成人免费观看男女羞羞视频 | 亚洲成人日韩 | 成人福利网站 | 欧美一区二区三区在线视频 | 成人免费淫片aa视频免费 | 国产欧美一区二区精品忘忧草 | 亚洲精品国产a久久久久久 午夜影院网站 | 男女免费网站 | 亚洲男人的天堂网站 | 99国产精品一区二区三区 | 91在线视频免费观看 |