系统登录失败次数超过限定次数,则根据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 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 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一起校验的锁定机制了。这两种方式结合校验的机制应该是挺完善的,按照上面的代码,我自己测试也是没啥问题的,当然可能我代码也会有遗漏的,欢迎大家评论补充。

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