设计登录接口,十分钟内连续登录5次失败,需要等待30分钟才能登录

2,699 阅读3分钟

今天在掘金上面看到一个大佬提了一个面试题,觉得挺有意思的,记录一下。

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 image.png