springboot中使用redis实现锁定异常登录的账户

很常见的一个场景:用户使用账户名密码登录的时候,如果输错了密码,会被系统记录错误的次数,当一定时间内,密码错误次数超过N次就会锁定账户一段时间。在这段时间内,该账户不能使用密码的方式进行登录。

目的是为了防止暴力破解密码,恶意请求等 。保护账户安全。

需求

用户在10分钟内,最多只能输错5次密码,否则锁定账户30分钟,在此期间不能通过密码的方式进行登录。

实现思路

账户的锁定,基于Redis的过期key实现。使用用户的账户作为Redis的key,登录先检查,Redis中是否有这个key的存在。如果存在,则表示账户已经被锁定 。不能使用密码的方式进行登录。这种锁定一般都是临时性的,所以这个Key是有过期时间的。

banned:{account}: 1

登录错误时,错误次数的统计,也是基于Redis的过期key实现,在Redis中以用户的账户作为key,错误次数作为value存储。在第1次设置的时候,设置它的过期时间。

failed:{account}: {count}

实现代码

import java.time.Duration;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/login")
public class LoginController {

	public static final String BANNED_KEY = "banned:";
	
	public static final String FAILED_KEY = "failed:";
	
	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	
	
	@PostMapping
	public Object login(@RequestParam("account") String account,
						@RequestParam("password") String password) {
		
		// 判断账户是否被锁定了
		String bannedKey = BANNED_KEY + account;
		if(this.stringRedisTemplate.hasKey(bannedKey)) {
			return Map.ofEntries(Map.entry("success", false), Map.entry("message", "错误次数太多,稍后再试或者选择其他的登录方式"));
		}
		
		// 登录成功 
		if (account.equals("admin") && password.equals("123456")) { // 假设了正确的账户名密码
			return Map.ofEntries(Map.entry("success", true));
		}
		
		// 记录登录失败的次数,并且判断是否要临时锁定账户
		String failedKey = FAILED_KEY + account;
		long count = this.stringRedisTemplate.opsForValue().increment(failedKey);
		if (count == 1) {
			// 第1次失败,设置key的过期时间,10分钟。
			this.stringRedisTemplate.expire(failedKey, Duration.ofMinutes(10));
		} else if(count >= 5){
			// 10分钟内,密码错误超过5次,锁定账户30分钟。
			this.stringRedisTemplate.opsForValue().set(bannedKey, "", Duration.ofMinutes(30));
		}
		
		return Map.ofEntries(Map.entry("success", false), Map.entry("message", "用户名密码错误"));
	}
}

实现的逻辑和代码都极其简单,测试结果也正常。节约篇幅,这里就不贴了。

最后

现在的应用,大都提供了不止一种的登录方式,Oatuh2,短信,扫码等等。禁止用户使用账户名密码登录后,应该让用户可以选择其他有效的方式进行登录。