前言
今天在掘金学习(moyu),看到一个文章,‘阿里面试官问我:十分钟内连续登录5次失败,需要等待30分钟才能登录’,觉得很有意思,虽然是一个普遍需求,但是解决方案非常多,于是自己半个小时画了个图,写了几行伪代码,来解一下这个需求,毕竟天天CRUD也挺无聊的,非最佳解决方案,仅供互相讨论学习,希望各位轻喷。
原文链接
阿里面试官问我:如何设计登录接口,十分钟内连续登录5次失败,需要等待30分钟才能登录 - 掘金 (juejin.cn)
直接上图
图解
- 登录请求
- 验证锁定换粗,锁定直接返回登录次数过多已锁定
- 未锁定 验证账号密码
- 验证失败 失败次数计数 次数=5,设置锁定缓存,返回失败
- 验证成功 删除计数缓存,返回成功
代码实现
package com.demo.login;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @auther 热黄油啤酒
*/
@Service
public class LoginService {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
private static final String FAIL_COUNTER = "user_login_fail_counter";
private static final String FAIL_LOCK = "user_login_fail_lock";
/**
* 登录操作
*
* @param username
* @param password
* @return
*/
public String login(String username, String password) {
// 1.验证用户是否被登录锁定
boolean lock = isLock(username);
if (lock) {
// 获取过期时间
long t = unlockTime(username);
return "登录验证失败次数过多,请" + t + "分钟后再试!";
}
boolean check = loginCheck(username, password);
if (!check) {
setCheckFailCounter(username);
return "登录失败!";
}
// 登录成功 移除失败计数器
deleteLoginFailCounter(username);
return "登录成功";
}
/**
* 登录失败计数器
*
* @param username
*/
public void setCheckFailCounter(String username) {
String key = String.join(":", FAIL_COUNTER, username);
Integer count = redisTemplate.opsForValue().get(key);
redisTemplate.opsForValue().increment(key);
if (count == null) {
// 第一次操作设置过期时间
redisTemplate.expire(key, 10, TimeUnit.MINUTES);
}
if (count.intValue() == 5) {
// 失败达到五次 设置锁定缓存
lock(username);
}
}
/**
* 移除计数器
*
* @param username
*/
public void deleteLoginFailCounter(String username) {
redisTemplate.delete(String.join(":", FAIL_COUNTER, username));
}
/**
* 失败达到一定一定次数 锁定30分钟
*
* @param username
*/
public void lock(String username) {
String key = String.join(":", FAIL_LOCK, username);
redisTemplate.opsForValue().set(key, 1, 30, TimeUnit.MINUTES);
}
/**
* 是否被登录锁定
*
* @param username
* @return
*/
public boolean isLock(String username) {
return redisTemplate.hasKey(String.join(":", FAIL_LOCK, username));
}
/**
* 获取解锁的时间
*
* @param username
* @return
*/
public long unlockTime(String username) {
String key = String.join(":", FAIL_LOCK, username);
return redisTemplate.opsForValue().getOperations().getExpire(key, TimeUnit.MINUTES);
}
/**
* 验证登录
*
* @param username
* @param password
* @return
*/
public boolean loginCheck(String username, String password) {
// 验证账号密码是否正确 省略...
return false;
}
}
复制代码
总结
使用两个缓存,一个登录失败计数缓存,一个锁定缓存,计数达到5触发计数缓存,未达到五继续计数,未达到5次之前成功,清空计数缓存。优点就是redis空间占用不多,每个用户就两个key,充分使用过期机制及redis自增数,在原需求的基础上还可以提醒用户多长时间后可以重新登录。希望大家友好沟通,互相进步。