登录次数统计是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设计:
- 登录次数缓存Key:
user:login:count:{userId}(如user:login:count:1001); - 锁定状态缓存Key:
user:login:lock:{userId}(存储锁定过期时间的时间戳); - 失败尝试缓存Key:
user: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());
}
}
安全增强策略
多维度安全控制:
- 频率限制:基于Redis实现滑动窗口算法,限制单位时间内的登录尝试次数
- IP限制:记录异常登录IP,实现基于IP的访问控制
- 验证码机制:在多次失败后触发图形或短信验证码验证
- 异地登录检测:结合用户登录地理位置信息,异常登录时发送通知
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);
}
}
缓存与数据库一致性保障
为确保数据一致性,采用以下策略:
- 双写策略:先更新缓存,再更新数据库
- 异步刷新:使用消息队列异步更新数据库
- 定期同步:定时任务将缓存数据