2022年1月7日更新: 既然大家的讨论热情这么高,我觉得就有必要把这篇文章再好好完善一下,评论区里大家给出的比较一致的实现方式是使用redis,我就把实现的思路也加进来吧。
part 1 面试白板编程实现(原生JDK)
常言道:字数越短问题越大。
今天阿里的面试官小哥哥让我实现一个登录接口,同一个用户10分钟内连续登陆5次失败,则需要等到30分钟才能登陆。
当然大佬估计一看到这种题目会很难过,一丁点算法都没有,妙解没意思。我上来就被唬住了。登录接口?10分钟内连续5次??等待30分钟才能登陆???登陆验证????
问号一下子就冒出来了,当然最开始我想定义一个变量firstFailTime
来记录第一次失败的时间,再仔细一想不对啊,firstFailTime
是动态的额,要不断变化,单一个变量不好实现啊,第一次登录失败可以记录,但如果出现前十分钟失败了4次,第11分钟又失败了一次的话,firstFailTime
应该往后取第二次失败登录的时间啊,我总不能手动定义100个变量吧。。。面试官看到估计脸都绿了。恨不得给我一个Mysql数据表,把每次登陆都给存下来,这样就可以很方便的查出某个时间区间登陆的情况。
不慌,咱们虽然不是大佬,但一点一点分析还是可以的,沉住气!等等,刚刚说到数据库存所有的登录数据??其实思考到上面已经快接近了,我不能手动创建100个变量,但我可以用一种数据结构依次记录登录失败的时间啊,突然想到LRU算法对不对!!能从数据顺序看出来时间顺序的数据结构不就是链表吗!!!还有登录验证的问题,不如偷个懒,用一个boolean控制。解决,cool~
P.S:我没考虑开多个线程去测试,因为我个人感觉用户登录不会出现在高并发的环境里,几万个人同时登陆同一个账号想想就离谱……但为了保险起见我还是给map加了synchronize关键字。
Person类:
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);
}
}
}
part 2 使用缓存来实现
实现步骤:
-
用户发起登录请求
-
后台验证是否失败次数过多,账户没有锁定的话就进入下面的步骤;否则直接返回
-
验证用户的账号 + 密码
3.1 验证成功:删除缓存 3.2 验证失败:统计最近10分钟时间窗口内的失败次数,如果达到5次则设置锁定缓存,返回
图解实现步骤:
代码实现细节:
-
登录失败计数器的key设计为:一串字符串 + 用户名(假设具有唯一性)+ 登录失败的时间
-
锁定登录操作的key设计为:一串字符串 + 用户名(假设具有唯一性)
private static final String FAIL_COUNT_REDIS_KEY = "login_fail_count";
private static final String LOCK_REDIS_KEY = "login_lock";
private static final String SEPARATOR = ":";
用户登录服务:
@Override
public String login(String username, String password) {
// 验证用户是否被登录锁定
boolean lock = isForbidden(username);
if (lock) {
return "Login authentication failed too many times. Please try again after " + unLockTime(username) + " minutes.";
}
// 验证用户名 + 密码
boolean isLogin = userRepository.checkUsernameAndPassword(username, password);
if (!isLogin) {
// 登录失败
setFailCounter(username);
return "login fail";
}
// 登录成功 移除失败计数器
deleteFilCounter(username);
return "login success";
}
登陆失败的话,就给登录失败次数加一:
@Override
public void setFailCounter(String username) {
// 获取当前时间
Calendar cal = Calendar.getInstance();
String minute = fastDateFormat.format(cal);
// 登录失败次数 + 1
String key = String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username, minute);
Integer count = redisTemplate.opsForValue().get(key);
redisTemplate.opsForValue().increment(key, 1); // 如果key不存在的话就会以增量形式存储进来
if (count == null) {
redisTemplate.expire(key, 10, TimeUnit.MINUTES);
}
// 如果失败次数大于5次,锁定账户
List<String> windowsKeys = new ArrayList<>();
for (int i = 0; i < 10; i ++) {
windowsKeys.add(String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username, fastDateFormat.format(cal)));
cal.add(Calendar.MINUTE, -1);
}
List<Integer> countList = redisTemplate.opsForValue().multiGet(windowsKeys);
assert countList != null;
int total = 0;
for (Integer c : countList) {
total += c;
}
if (total >= maxFailTimes) {
forbidden(username);
}
}
如果登录成功,则删除失败次数计数器:
@Override
public void deleteFilCounter(String username) {
Calendar cal = Calendar.getInstance();
List<String> windowKeys = new ArrayList<>();
for (int i = 0; i < 10; i ++) {
windowKeys.add(String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username, fastDateFormat.format(cal)));
cal.add(Calendar.MINUTE, -1);
}
redisTemplate.delete(windowKeys);
}
失败次数超过5次则禁止登录,只需要设置一个缓存即可:
@Override
public void forbidden(String username) {
String key = String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username);
redisTemplate.opsForValue().set(key, 1, 30, TimeUnit.MINUTES);
}
判断是否被禁止登录,只需要判断是否有上面方法设置的key即可:
@Override
public boolean isForbidden(String username) {
try{
return Boolean.TRUE.equals(redisTemplate.hasKey(String.join(SEPARATOR, FAIL_COUNT_REDIS_KEY, username)));
}catch (Exception e){
e.printStackTrace();
}
return false;
}
如果想要获取到用户具体需要几分钟才能解锁(用于提示信息),只需要查询缓存的过期时间:
private Long unLockTime(String username){
String key = String.join(SEPARATOR, LOCK_REDIS_KEY, username);
Long expireTime = redisTemplate.opsForValue().getOperations().getExpire(key, TimeUnit.MINUTES);
if (expireTime == null){
throw new RuntimeException("there is no unlock time");
}
return expireTime;
}
今天的文章阿里面试官问我:如何设计登录接口,十分钟内连续登录5次失败,需要等待30分钟才能登录分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21252.html