系统登录失败次数超过限定次数,则根据IP或用户名锁定,需要过了锁定时间才可以继续登录

前言

之前做的项目都有用户名锁定机制,即:用户名失败次数超过多少次,就锁定这个用户不可以再登录,需要等过了锁定时间才可以继续登录。

然后最近的一个项目中,有个漏洞整改措施中,提到了这个锁定机制不能只根据用户名锁定,还要根据IP锁定。

两种锁定机制

1、根据用户名锁定

根据用户名锁定的前提是这个用户名要存在,在数据库中查出了这个用户名,我们才能记录错误次数,从而判断是否应该锁定。那假如这个用户名不存在,那就不用记录错误次数,那是不是可以一直调用登录接口?想象一下,某个恶意IP一直无限次调用登录接口,每次调用都会查询一次数据库,这。。。。。。


2、根据IP锁定

如果只根据IP锁定的话,就可能会存在某些恶意用户通过不停切换IP的方式,对某个用户名的密码进行猜测的情况。因为IP锁定只能对IP进行锁定,而无法对具体的用户名进行锁定。


综合以上两种锁定机制的缺点,所以我们不能只采用其中一种,而是需要结合两种锁定方式来进行校验。

实现流程

既然知道了两种机制单独使用的缺点,那我们的实现流程也好梳理了。

1、不管用户名是否存在,先校验IP是否已经锁定,锁定了不执行后面的流程,直接提示错误信息。


2、 如果IP没有被锁定,则先查询用户名,用户名不存在,则记录IP错误次数+1,并判断错误次数是否达到限定次数,达到则锁定,并提示错误信息;该IP下一次登录时,则会执行第一步。


3、 如果用户名存在,则校验用户名是否已经锁定,锁定了不执行后面的流程,直接提示错误信息。


4、如果用户名没有被锁定,则校验密码是否正确,密码错误,则同时记录IP错误次数+1、用户名错误次数+1,并判断错误次数是否达到限定次数,达到则锁定,并提示错误信息;该IP或用户名下一次登录时,则会执行第一步或第三步。


5、如果密码正确,则清除当前用户名和IP的错误次数记录。

以上五点中,其中第四点关于判断错误次数是否达到限定次数,我是同时要获取IP错误次数和用户名错误次数,取这两个次数中较大的那一个为准,如果相等就随便取一个。

假如IP错误次数较大,则IP锁定了,那不管你用哪个用户名只要是这个IP,就都不允许登录;如果是用户名错误次数较大,则用户名锁定了,那不管你怎么改变IP,只要是这个用户名,就不允许登录。

代码

梳理好了流程,那我们就可以开始写代码了

登录的controller

/** 密码最大错误次数 */
private int ERROR_COUNT = 3;
/** 锁定时长 */
private String LOCK_DURATION = "15";

@PostMapping("/login")
public ResultUtil login(String userName, String password,HttpServletRequest request){
    String ip = IPUtil.getIpAddress(request);
    long currentTime = System.currentTimeMillis();
    lockedUser(currentTime, ip,"IP"); //判断ip是否锁定
    //保存登录日志
    SysLog sysLog = new SysLog(ip,"用户登录","login");
    sysLog.setId(IdUtil.getSnowflakeNextIdStr());
    sysLog.setState("登录成功");
    try {
        //私钥解密
        userName = RSAUtil.decrypt(userName);
        password = RSAUtil.decrypt(password);

        sysLog.setCreatorId(userName);
        SysSafe safe = sysSafeService.list().get(0);
        SysUser user = passwordErrorNum(ip,userName, password,safe);// 先查询用户名是否存在,不存在则校验IP,存在则校验用户名和密码
        int i = safe.getIdleTimeSetting(); //如果系统闲置时间为0,设置token和session永不过期
        String token = "";
        if (i==0){
            token = LoginUtil.login(user,null,2592000);// 最长保持登录为30天
        }else {
            token = LoginUtil.login(user);
        }
        sysLog.setInfo(userName+"登录成功");
        sysLogService.save(sysLog);
        return ResultUtil.success(token);
    } catch (ExceptionVo e) {
        sysLog.setInfo(e.getMessage());
        sysLog.setState("登录失败");
        sysLogService.save(sysLog);
        return ResultUtil.error(e.getCode(),e.getMessage());
    }catch (Exception e) {
        sysLog.setInfo(BaseConstant.UNKNOWN_EXCEPTION);
        sysLog.setState("登录失败");
        sysLogService.save(sysLog);
        e.printStackTrace();
        return ResultUtil.error(BaseConstant.UNKNOWN_EXCEPTION);
    }
}

// ......省略其他接口
// 注意,如果有获取验证码或获取公钥的接口(这两个接口都是在登录页面加载时、调用登录接口之前调用的),也需要先校验IP是否锁定,锁定了不给返回新数据。如下:
/*String ip = IPUtil.getIpAddress(request);
long currentTime = System.currentTimeMillis();
lockedUser(currentTime, ip,"IP"); //判断ip是否锁定*/

/**
 * 判断账号或IP是否锁定
 */
private boolean lockedUser(long currentTime,String userName,String msg){
    boolean flag = false;
    if (RedisUtil.hasKey(BaseConstant.ERROR_COUNT+userName)){
        long loginTime = Long.parseLong(RedisUtil.hget(BaseConstant.ERROR_COUNT+userName, "loginTime").toString());
        String isLocaked = RedisUtil.hget(BaseConstant.ERROR_COUNT+userName,"isLocaked").toString();
        if ("true".equals(isLocaked) && currentTime < loginTime){
            Duration between = LocalDateTimeUtil.between(LocalDateTimeUtil.of(currentTime), LocalDateTimeUtil.of(loginTime));
            throw new ExceptionVo(1004,msg+"锁定中,还没到允许登录的时间,请"+between.toMinutes()+"分钟后再尝试");
        }else{
            flag = true;
            RedisUtil.hset(BaseConstant.ERROR_COUNT+userName,"isLocaked","false");//重置为false
        }
    }
    return flag;
}

/**
 * 账号和密码错误次数验证
 */
private SysUser passwordErrorNum(String ip,String userName, String password,SysSafe sysSafe) throws InvalidKeySpecException, NoSuchAlgorithmException {
    //查询用户
    SysUser user = sysUserService.getUser(null,userName);
    if (null == user){ // 根据用户名查询用户,如果没有查到,则根据ip校验
        checkIPLocked(sysSafe,ip);
    }
    long currentTime = System.currentTimeMillis();
    boolean flag = lockedUser(currentTime, userName,"账号");//判断账号是否锁定
	//根据前端输入的密码(明文),和加密的密码、盐值进行比较,判断输入的密码是否正确
    boolean authenticate = EncryptionUtil.authenticate(password, user.getPassword(), user.getSalt());
    if (authenticate) {
        //密码正确错误次数和IP错误次数清零
        RedisUtil.del(BaseConstant.ERROR_COUNT+userName);
        RedisUtil.del(BaseConstant.ERROR_COUNT+ip);
    } else {
        checkNameLocked(sysSafe,userName,ip,flag);
    }
    return user;
}

/**
 * 校验IP锁定
 */
public boolean checkIPLocked(SysSafe sysSafe,String ip){
    long currentTime = System.currentTimeMillis();
    boolean flag = lockedUser(currentTime, ip,"IP");//判断IP是否锁定
    //错误3次,锁定15分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
    long timeStamp = System.currentTimeMillis()+900000;
    //密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)
    if (sysSafe.getPwdLoginLimit()==1){
        ERROR_COUNT = 5;
        LOCK_DURATION = "30";
        //错误5次,锁定30分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
        timeStamp = System.currentTimeMillis()+1800000;
    }
    if (RedisUtil.hasKey(BaseConstant.ERROR_COUNT+ip)){
        int i = Integer.parseInt(RedisUtil.hget(BaseConstant.ERROR_COUNT+ip,"errorNum").toString());
        if (flag && i==ERROR_COUNT){ // 当错误次数达到限定次数时,走到这一步说明已经过了锁定时间再次登录,这时重新将错误次数设置为1
            RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"errorNum",1);
        }else {
            RedisUtil.hincr(BaseConstant.ERROR_COUNT+ip,"errorNum",1);// 错误次数加一
        }
        RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"loginTime",timeStamp);
    }else {
        Map<String,Object> map = new HashMap<>();
        map.put("errorNum",1);
        map.put("loginTime",timeStamp);
        map.put("isLocaked","false");// 是否锁定,默认为false
        RedisUtil.hmset(BaseConstant.ERROR_COUNT+ip, map, -1);
    }
    int i = Integer.parseInt(RedisUtil.hget(BaseConstant.ERROR_COUNT+ip,"errorNum").toString());
    if (i==ERROR_COUNT){
    	// 将锁定状态改为true表示已锁定
       	RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"isLocaked","true");
        throw new ExceptionVo(1004,"用户名或密码错误"+ERROR_COUNT+"次,现已被锁定,请"+LOCK_DURATION+"分钟后再尝试");
    }
    throw new ExceptionVo(1000,"用户名或密码错误,总登录次数"+ERROR_COUNT+"次,剩余次数: " + (ERROR_COUNT-i));
}

/**
 * 校验用户名锁定
 */
public boolean checkNameLocked(SysSafe sysSafe,String userName,String ip,boolean flag){
    //错误3次,锁定15分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
    long timeStamp = System.currentTimeMillis()+900000;
    //密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)
    if (sysSafe.getPwdLoginLimit()==1){
        ERROR_COUNT = 5;
        LOCK_DURATION = "30";
        //错误5次,锁定30分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
        timeStamp = System.currentTimeMillis()+1800000;
    }
    if (RedisUtil.hasKey(BaseConstant.ERROR_COUNT+userName)){
        int i1=0,i2=0;
        if (RedisUtil.hasKey(BaseConstant.ERROR_COUNT+userName))
            i1 = Integer.parseInt(RedisUtil.hget(BaseConstant.ERROR_COUNT+userName,"errorNum").toString());
        if (RedisUtil.hasKey(BaseConstant.ERROR_COUNT+ip))
            i2 = Integer.parseInt(RedisUtil.hget(BaseConstant.ERROR_COUNT+ip,"errorNum").toString());
        // 每一次错误,同时记录当前IP和用户名的错误次数
        if (flag && (i1==ERROR_COUNT || i2==ERROR_COUNT)){ // 走到这一步说明已经过了锁定时间再次登录,这时重新将错误次数设置为1
            if (i1>i2){ // i1 > i2 是用户名错误次数到达限定次数,将用户名的错误次数重置为1
                RedisUtil.hset(BaseConstant.ERROR_COUNT+userName,"errorNum",1);
            }else if (i2>i1){ // i2 > i1 是IP错误次数到达限定次数,将IP的错误次数重置为1
                RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"errorNum",1);
            }else { // 否则就是用户名和IP错误次数相等,将两个的错误次数同时重置为1
                RedisUtil.hset(BaseConstant.ERROR_COUNT+userName,"errorNum",1);
                RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"errorNum",1);
            }
        }else {
            RedisUtil.hincr(BaseConstant.ERROR_COUNT+userName,"errorNum",1);
            RedisUtil.hincr(BaseConstant.ERROR_COUNT+ip,"errorNum",1);
        }
        RedisUtil.hset(BaseConstant.ERROR_COUNT+userName,"loginTime",timeStamp);
        RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"loginTime",timeStamp);
    }else {
        Map<String,Object> map = new HashMap<>();
        map.put("errorNum",1);
        map.put("loginTime",timeStamp);
        map.put("isLocaked","false");
        RedisUtil.hmset(BaseConstant.ERROR_COUNT+userName, map, -1);
        if (!RedisUtil.hasKey(BaseConstant.ERROR_COUNT+ip)){
            RedisUtil.hmset(BaseConstant.ERROR_COUNT+ip, map, -1);
        }
    }
    int i1 = Integer.parseInt(RedisUtil.hget(BaseConstant.ERROR_COUNT+userName,"errorNum").toString());
    int i2 = Integer.parseInt(RedisUtil.hget(BaseConstant.ERROR_COUNT+ip,"errorNum").toString());
    int i = i1 >= i2 ? i1 : i2;// 取错误次数大的那个值进行判断
    if (i1==ERROR_COUNT || i2==ERROR_COUNT){ // 任意一个满足,将值大的那个设置为锁定
        if (i1>i2){ // i1 > i2 是用户名错误次数到达限定次数,将用户名的锁定状态设置为锁定
            RedisUtil.hset(BaseConstant.ERROR_COUNT+userName,"isLocaked","true");
        }else if (i2>i1){ // i2 > i1 是IP错误次数到达限定次数,将IP的锁定状态设置为锁定
            RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"isLocaked","true");
        }else { // 否则就是用户名和IP错误次数相等,将两个的锁定状态同时设置为锁定
            RedisUtil.hset(BaseConstant.ERROR_COUNT+userName,"isLocaked","true");
            RedisUtil.hset(BaseConstant.ERROR_COUNT+ip,"isLocaked","true");
        }
        throw new ExceptionVo(1004,"用户名或密码错误"+ERROR_COUNT+"次,现已被锁定,请"+LOCK_DURATION+"分钟后再尝试");
    }
    throw new ExceptionVo(1000,"用户名或密码错误,总登录次数"+ERROR_COUNT+"次,剩余次数: " + (ERROR_COUNT-i));
}

以上我们就实现了用户名和IP一起校验的锁定机制了。这两种方式结合校验的机制应该是挺完善的,按照上面的代码,我自己测试也是没啥问题的,当然可能我代码也会有遗漏的,欢迎大家评论补充。

最后,如果这篇文章你觉得写得还行或者对你有点帮助的话,欢迎给点个大拇指~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

符华-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值