不能锁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