Java登录次数统计

admin 104 0
登录次数统计是Java应用中的基础功能,常用于用户行为分析、安全防护及系统监控,实现上可采用数据库(如MySQL)存储登录日志,结合缓存(Redis)提升统计效率;或使用ConcurrentHashMap实现线程安全的内存计数,通过按用户、时间、IP等维度统计,可分析用户活跃度、识别异常登录(如频繁失败),为风控系统提供数据支撑,同时助力后台监控登录趋势,优化用户体验与系统安全性。

Java实现用户登录次数统计与安全控制实践

在信息系统中,用户登录作为身份认证的核心环节,其登录次数的统计与管理直接关系到系统的安全性与用户体验,本文将基于Java技术栈,从数据存储、核心逻辑、安全增强等维度,详细介绍如何实现一套完整的用户登录次数统计与安全控制方案。

技术方案设计:数据存储与核心逻辑

数据存储:数据库与缓存的协同

登录次数统计需要兼顾实时性持久化需求:数据库负责长期存储,缓存(如Redis)则支撑高频读写,我们采用"数据库+缓存"的双存储架构:

(1)数据库表设计(MySQL)

以用户表(user)为例,增加登录次数相关字段:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '加密密码',
  `login_count` int(11) DEFAULT 0 COMMENT '累计登录次数',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `is_locked` tinyint(1) DEFAULT 0 COMMENT '是否锁定(0:否,1:是)',
  `lock_expire_time` datetime DEFAULT NULL COMMENT '锁定过期时间',
  `failed_login_attempts` int(11) DEFAULT 0 COMMENT '失败登录尝试次数',
  `last_failed_time` datetime DEFAULT NULL COMMENT '最后失败登录时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
);

字段说明:

  • login_count:记录用户历史登录总次数(成功登录);
  • is_locked:标记账户是否因异常登录被锁定;
  • lock_expire_time:锁定状态的过期时间,支持自动解锁;
  • failed_login_attempts:记录连续失败登录尝试次数,用于触发安全策略;
  • last_failed_time:记录最后一次失败登录时间,用于实现时间窗口内的限制。
(2)缓存设计(Redis)

利用Redis的高性能特性,存储实时登录次数临时锁定状态,减少数据库压力:

缓存Key设计:

  • 登录次数缓存Keyuser:login:count:{userId}(如user:login:count:1001);
  • 锁定状态缓存Keyuser:login:lock:{userId}(存储锁定过期时间的时间戳);
  • 失败尝试缓存Keyuser:login:failed:{userId}(记录当前时间窗口内的失败次数);
  • 缓存过期时间
    • 登录次数缓存:24小时(可根据业务需求调整)
    • 锁定状态缓存:与lock_expire_time保持一致
    • 失败尝试缓存:15分钟(实现短时间内的登录限制)

核心逻辑实现

登录验证流程
@Service
public class LoginService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private PasswordEncoder passwordEncoder;
    // 最大允许失败次数
    private static final int MAX_FAILED_ATTEMPTS = 5;
    // 锁定时间(分钟)
    private static final int LOCK_TIME_DURATION = 30;
    public LoginResult login(String username, String password) {
        // 1. 检查用户是否存在
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UserNotFoundException("用户不存在"));
        // 2. 检查账户是否被锁定
        if (isAccountLocked(user.getId())) {
            throw new AccountLockedException("账户已被锁定,请稍后再试");
        }
        // 3. 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            handleFailedLogin(user);
            throw new InvalidPasswordException("密码错误");
        }
        // 4. 登录成功处理
        handleSuccessfulLogin(user);
        return new LoginResult("登录成功", user);
    }
    private boolean isAccountLocked(Long userId) {
        // 检查缓存中的锁定状态
        Object lockExpire = redisTemplate.opsForValue().get("user:login:lock:" + userId);
        if (lockExpire != null) {
            long expireTime = Long.parseLong(lockExpire.toString());
            if (expireTime > System.currentTimeMillis()) {
                return true;
            }
            // 锁定已过期,清除缓存
            redisTemplate.delete("user:login:lock:" + userId);
        }
        // 检查数据库中的锁定状态
        return user.getIsLocked() && 
               user.getLockExpireTime() != null && 
               user.getLockExpireTime().after(new Date());
    }
    private void handleFailedLogin(User user) {
        // 更新数据库中的失败次数
        user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1);
        user.setLastFailedTime(new Date());
        // 检查是否达到最大失败次数
        if (user.getFailedLoginAttempts() >= MAX_FAILED_ATTEMPTS) {
            // 锁定账户
            lockAccount(user);
        }
        userRepository.save(user);
        // 更新缓存中的失败次数
        String failedKey = "user:login:failed:" + user.getId();
        redisTemplate.opsForValue().increment(failedKey);
        redisTemplate.expire(failedKey, 15, TimeUnit.MINUTES);
    }
    private void lockAccount(User user) {
        user.setIsLocked(true);
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, LOCK_TIME_DURATION);
        user.setLockExpireTime(calendar.getTime());
        // 设置缓存中的锁定状态
        String lockKey = "user:login:lock:" + user.getId();
        redisTemplate.opsForValue().set(lockKey, 
            calendar.getTimeInMillis(), 
            LOCK_TIME_DURATION, TimeUnit.MINUTES);
    }
    private void handleSuccessfulLogin(User user) {
        // 重置失败次数
        user.setFailedLoginAttempts(0);
        user.setLastFailedTime(null);
        // 更新登录次数和时间
        user.setLoginCount(user.getLoginCount() + 1);
        user.setLastLoginTime(new Date());
        userRepository.save(user);
        // 清除相关缓存
        redisTemplate.delete("user:login:failed:" + user.getId());
    }
}
安全增强策略

多维度安全控制:

  1. 频率限制:基于Redis实现滑动窗口算法,限制单位时间内的登录尝试次数
  2. IP限制:记录异常登录IP,实现基于IP的访问控制
  3. 验证码机制:在多次失败后触发图形或短信验证码验证
  4. 异地登录检测:结合用户登录地理位置信息,异常登录时发送通知

IP限制实现示例:

@Component
public class IpSecurityService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    private static final int MAX_LOGIN_ATTEMPTS_PER_IP = 10;
    private static final int IP_LOCK_DURATION = 60; // 分钟
    public boolean checkIpAllowed(String ip) {
        String ipKey = "security:ip:attempts:" + ip;
        Object attempts = redisTemplate.opsForValue().get(ipKey);
        if (attempts == null) {
            redisTemplate.opsForValue().set(ipKey, 1, 1, TimeUnit.HOURS);
            return true;
        }
        int attemptCount = Integer.parseInt(attempts.toString());
        if (attemptCount >= MAX_LOGIN_ATTEMPTS_PER_IP) {
            // IP已被锁定
            return false;
        }
        redisTemplate.opsForValue().increment(ipKey);
        return true;
    }
    public void lockIp(String ip) {
        String ipKey = "security:ip:lock:" + ip;
        redisTemplate.opsForValue().set(ipKey, "locked", 
            IP_LOCK_DURATION, TimeUnit.MINUTES);
    }
}

缓存与数据库一致性保障

为确保数据一致性,采用以下策略:

  1. 双写策略:先更新缓存,再更新数据库
  2. 异步刷新:使用消息队列异步更新数据库
  3. 定期同步:定时任务将缓存数据