前言
之前做的项目都有用户名锁定机制,即:用户名失败次数超过多少次,就锁定这个用户不可以再登录,需要等过了锁定时间才可以继续登录。
然后最近的一个项目中,有个漏洞整改措施中,提到了这个锁定机制不能只根据用户名锁定,还要根据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一起校验的锁定机制了。这两种方式结合校验的机制应该是挺完善的,按照上面的代码,我自己测试也是没啥问题的,当然可能我代码也会有遗漏的,欢迎大家评论补充。文章来源:https://www.toymoban.com/news/detail-663378.html
最后,如果这篇文章你觉得写得还行或者对你有点帮助的话,欢迎给点个大拇指~文章来源地址https://www.toymoban.com/news/detail-663378.html
到了这里,关于系统登录失败次数超过限定次数,则根据IP或用户名锁定,需要过了锁定时间才可以继续登录的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!