自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java 中實現(xiàn)用戶登錄次數(shù)限制的多種方案

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

前言

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

使用 HashMap 和 Timer 實現(xiàn)

首先,我們創(chuàng)建一個 HashMap 來存儲用戶登錄失敗的信息。這個 HashMap 中的鍵是用戶名,而值是一個自定義的 LoginAttempt 對象,它包含登錄失敗次數(shù)和最近一次失敗時間。當用戶進行登錄操作時,我們會檢查 HashMap 中是否存在該用戶的記錄。若存在,會查看是否超過 5 分鐘,如果超過,我們將重置失敗次數(shù);如果未超過且次數(shù)已達 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");
                // 模擬登錄成功,重置嘗試次數(shù)
                loginAttempts.remove(username);
            } else {
                System.out.println("Login not allowed, user locked");
            }
            recordFailedLogin(username);
        }
    }
}

使用 ConcurrentHashMap 和 ScheduledExecutorService 實現(xiàn)

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

示例

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");
                // 模擬登錄成功,重置嘗試次數(shù)
                loginAttempts.remove(username);
            } else {
                System.out.println("Login not allowed, user locked");
            }
            recordFailedLogin(username);
        }
    }
}

使用 Redis 存儲登錄嘗試信息

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

示例

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);
        }
    }
}

補充-滑動窗口

構(gòu)造一個滑動窗口,窗口大小限制5分鐘,然后限流次數(shù)設(shè)置為3次即可實現(xiàn)這個功能了。而滑動窗口我們可以借助Redis來實現(xiàn)。

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private String key;
    private int limit = 3; //限制請求次數(shù)最大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" +  // 獲取當前請求次數(shù)
                "if current_requests < tonumber(ARGV[2]) then\n" +  // 如果請求次數(shù)小于限制
                "    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";
        // 調(diào)用 Lua 腳本進行原子操作
        Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(limit), String.valueOf(lockTime));
        // 返回操作結(jié)果
        return (Long) result == 1;
    }
}

使用 Spring Security 實現(xiàn)

我們還可以使用 Spring Security 的相關(guān)組件來實現(xiàn)。利用 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();
        }
    }
}

最后

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

責任編輯:武曉燕 來源: 一安未來
相關(guān)推薦

2018-04-02 10:16:00

bug代碼安卓

2009-12-30 09:45:52

Silverlight

2009-12-23 10:46:38

WPF實現(xiàn)用戶界面

2012-05-04 09:28:49

Linux

2010-01-28 10:00:54

linux用戶注銷logout

2010-08-04 10:48:17

路由器

2021-08-05 10:40:37

加密方案Spring

2024-09-22 10:46:33

數(shù)據(jù)飛輪算法

2016-10-24 23:18:55

數(shù)據(jù)分析漏斗留存率

2024-04-08 14:10:06

2011-03-24 08:56:23

escalationsNagios報警

2009-09-07 09:20:34

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

用戶注冊用戶重復(fù)數(shù)據(jù)

2010-06-18 13:52:24

SQL Server查

2011-03-25 14:56:43

Nagiosescalations
點贊
收藏

51CTO技術(shù)棧公眾號