今天在掘金上面看到一个大佬提了一个面试题,觉得挺有意思的,记录一下。
1.需求:
实现一个登录接口,同一个用户10分钟内连续登陆5次失败,则需要等到30分钟才能登陆。
2.思路
初看这个问题似乎很简单,只要记录十分钟内登录的次数,在每次登录的时候校验一下就可以了。细想一下,还得考虑时间线的问题,需求限制的10分钟内登录五次失败的操作,应该是以当前的操作时间为时间点,往前推10分钟内的登录次数,时间点是动态的;同时还要考虑在实际的项目中的用户量大等问题,总结大佬的博客以及评论,总结了以下的解决方案。
3.方案
3.1 原生jdk(不考虑性能)
这里给出大佬的代码(juejin.cn/post/702884… )
package exam;
import java.util.LinkedList;
/**
* Created by Enzo Cotter on 2021/3/10.
*/
public class Person {
/**
* 重置时间
*/
private static final int RESET_TIME = 30;
/**
* 密码连续输入5次失败的持续时间
*/
private static final int DURATION = 10;
/**
* 最大输入失败次数
*/
private static final int MAX_TIMES = 5;
/**
* 用户id
*/
private String id;
/**
* 登录失败次数
*/
private int failCount;
/**
* 第一次失败的时间
*/
private long firstFailTime;
/**
* 登录失败的时间
*/
private LinkedList<Long> times;
private boolean lock;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getFailCount() {
return failCount;
}
public void setFailCount(int failCount) {
this.failCount = failCount;
}
public long getFirstFailTime() {
return firstFailTime;
}
public void setFirstFailTime(long firstFailTime) {
this.firstFailTime = firstFailTime;
}
public LinkedList<Long> getTimes() {
return times;
}
public void setTimes(LinkedList<Long> times) {
this.times = times;
}
public Person() {
}
public Person(String id, int failCount, long firstFailTime, LinkedList<Long> times, boolean lock) {
this.id = id;
this.failCount = failCount;
this.firstFailTime = firstFailTime;
this.times = times;
this.lock = false;
}
/**
* 密码输错了进入此方法
*/
public void isValid(){
long thisTime = System.currentTimeMillis() / 1000;
System.out.println("第一次登录失败时间" + thisTime);
// 超过30分钟,重置
if(thisTime > firstFailTime + RESET_TIME){
this.failCount = 1;
firstFailTime = thisTime;
times = new LinkedList<>();
times.addLast(thisTime);
this.lock = false;
return;
}else{ // 没有超过30分钟
if (lock){
System.out.println("账户锁定,请" + RESET_TIME + "分钟后再来");
return;
}
// 之前记录的第一次登录失败时间在10分钟之前了,要换
while(!times.isEmpty() && thisTime > times.getFirst() + DURATION){
times.removeFirst();
this.failCount --;
this.firstFailTime = times.isEmpty() ? thisTime : times.getFirst();
}
if(this.failCount >= 5 && thisTime < firstFailTime + DURATION){
System.out.println("10分钟内密码错误大于等于5次,登录失败");
times.addLast(thisTime);
this.lock = true;
}else if(failCount < MAX_TIMES){
this.failCount ++;
System.out.println("密码错误" + this.failCount + "次");
times.addLast(thisTime);
}
}
}
}
package exam;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* Created by Enzo Cotter on 2021/3/10.
*/
public class FlowLimit {
private static Map<String, Person> map = new HashMap<>();
/**
* 登录
* @param id
* @param flag 是否成功
*/
public static void login(String id, boolean flag){
if (flag){
// 登陆成功
return;
}else{
Person p = null;
// 登录失败
synchronized (map) {
p = map.get(id);
if (p == null){
p = new Person(id, 0, System.currentTimeMillis() / 1000,
new LinkedList<>(), false);
map.put(id, p);
return;
}
p.isValid();
}
}
}
public static void main(String[] args) {
for(int i = 0; i < 20; i ++){
login("aaa", false);
}
}
}
3.2 使用数据库保存所有的登录记录
创建loginLog表,设置用户id、登陆时间、登陆状态三个字段;每次登陆不管成功与否都要插入一条记录;在插入之前先检查十分钟内登陆失败记录条数,如果小于5,则可以继续登陆。否则查看最后一条记录的时间,和当前对比,如果是30分钟以内,就不再进行鉴权,直接禁止登陆;
3.3 使用Redis保存登录记录
- 用redis, key=userid_$random, 设置一个10分钟过期时间. 然后每次登陆时统计该用户的登录数量, 大于等于5就禁止登陆
写在最后
其实每种方法都有他的优点和缺点,也还有改进的空间
- 原生jdk的方法代码的思路十分清晰,值得学习,但在实际项目中肯定是用不了的
- 使用数据库保存登录记录的方法很方便,但是在用户量较多,登录次数较多的时候,也会存在数据量大的问题。时间长了后登录记录表会存在很多的无用数据,查询十分费时。相应就提出使用定时任务删除数据库无效数据的解决方案
- 使用Redis的时候,注意不要使用keys方法,可以用scan代替
www.redis.com.cn
www.redis.cn