前言
在登录模块出现暴力碰撞测试的安全漏洞时,我们最常见的方案就是需要记录账密错误,当达到一定错误阈值(比如5次)时,就会锁定该账号一定时间(如10分钟),等到锁定时间过了之后再允许用户继续登录,这样就能有效的抵御恶意暴力破解系统登录账号密码。
而具体实现又有很多种方式,最常见的就是如果系统框架引入了redis分布式缓存,就可以直接将信息缓存到redis中;也可以直接将错误次数,错误时间持久化到数据库中,每次登录校验账密之前都从数据库中查出来做判断逻辑;而今天我们讨论的是最简单的本地缓存方式,利用GuavaCache将本地数据缓存到JVM内存中,实现简单的本地数据缓存。
一、暴力破解漏洞描述
暴力破解-攻击者可利用该漏洞无限次提交用户名密码,从而可以暴力破解系统用户名及密码,如果暴力破解成功,攻击者可以登录到系统进行管理和查看网站敏感信息。
二、Guava cache简介
Guava cache是一个支持高并发的线程安全的本地缓存。多线程情况下也可以安全的访问或者更新Cache。这些都是借鉴了ConcurrentHashMap的结果,不过,guava cache 又有自己的特性 :
“automatic loading of entries into the cache”
即 :当cache中不存在要查找的entry的时候,它会自动执行用户自定义的加载逻辑,加载成功后再将entry存入缓存并返回给用户未过期的entry,如果不存在或者已过期,则需要load,同时为防止多线程并发下重复加载,需要先锁定,获得加载资格的线程(获得锁的线程)创建一个LoadingValueRefrerence并放入map中,其他线程等待结果返回。
三、GuavaCache本地缓存实现
1.测试实现方法
代码如下(示例):
package com.dd.pp.user.auth.server;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @Author:张绪升
* @Date:2024/8/30 11:19
*/
//@SpringBootTest
@Slf4j
public class GuavaCacheTest {
// 使用Guava Cache来存储用户名和解锁时间
private LoadingCache<String, Integer> userLockUntilTimes;
// 初始化一个缓存实例
public void userLockService(int lockDurationMinutes, int maximumSize) {
this.userLockUntilTimes = CacheBuilder.newBuilder()
.expireAfterAccess(lockDurationMinutes, TimeUnit.SECONDS)
.maximumSize(maximumSize)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
// 初始化时间为0,表示用户未被锁定
return 0;
}
});
}
/**
* 验证用户密码是否正确,如果错误,查询缓存中最新错误次数,并执行+1操作后更新缓存数据
* @param userName 用户名
* @param password 密码
* @param lockDurationMinutes 锁定时间(分钟)
* @return 是否锁定用户
*/
@Synchronized
public boolean validatePassword(String userName, String password) throws ExecutionException {
// 假设checkPassword是检查密码的方法
boolean isPasswordCorrect = checkPassword(userName, password);
Integer count = 0;
if (!isPasswordCorrect) {
count = userLockUntilTimes.get(userName);
// 如果密码错误,将用户名加入缓存
count ++;
userLockUntilTimes.put(userName, count);
log.info("错误次数:" + count);
}
return !isPasswordCorrect;
}
/**
* 检查用户是否被锁定
* @param userName 用户名
* @return 是否被锁定
*/
public boolean isUserLocked(String userName) throws ExecutionException {
//密码第4次错误
Integer count = userLockUntilTimes.get(userName);
if (count > 3){
// 如果错误次数大于3,用户被锁定
return true;
}
// 用户未被锁定或者锁定已过期
return false;
}
// 模拟检查密码的方法
private boolean checkPassword(String userName, String password) {
// 实际情况应该查询数据库或者其他认证系统
return "correctPassword".equals(password);
}
// 模拟输入错误帐密的登录请求
@Test
public void passwordTest() throws ExecutionException {
boolean isError;
boolean isLocked;
String userName = "user1";
String password = "wrongPassword";
Integer errorCount = userLockUntilTimes.get(userName);
if (errorCount > 3){
log.info("用户已锁定,请稍后再试!");
}else {
isError = this.validatePassword(userName, password);
isLocked = this.isUserLocked(userName);
if (!isError){
log.info("用户登录成功");
}
else if (isError && !isLocked) {
log.info("用户密码错误");
}
else if (isError && isLocked) {
log.info("用户已锁定,请稍后再试");
}
}
}
// 模拟输入正确帐密的登录请求
@Test
public void passwordTrue() throws ExecutionException {
boolean isError;
boolean isLocked;
String userName = "user1";
String password = "correctPassword";
Integer errorCount = userLockUntilTimes.get(userName);
log.info("当前错误次数:{}", errorCount);
if (errorCount > 3){
log.info("用户已锁定,请稍后再试!");
}else {
isError = this.validatePassword(userName, password);
isLocked = this.isUserLocked(userName);
if (!isError){
log.info("用户登录成功");
}
else if (isError && !isLocked) {
log.info("用户密码错误");
}
else if (isError && isLocked) {
log.info("用户已锁定,请稍后再试");
}
}
}
// 模拟多线程登录操作
private void newThreadRun(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
log.info("passwordTest starting");
Thread.sleep(100);
passwordTest();
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
private void newThreadRunTrue(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
log.info("passwordTrue starting");
Thread.sleep(100);
passwordTrue();
log.info("passwordTrue end");
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
/*
* 模拟多次登录,连续输入错误密码四次,第四次错误会提示用户用户已锁定,
* 10s之后缓存失效,用户自动解锁再输入正确账密登录一次
*/
@Test
void loginTest() throws ExecutionException, InterruptedException {
// 过期时间10s,缓存数量最多1000个
this.userLockService(10, 1000);
// 第一次登录失败
newThreadRun();
// 第二次登录失败
newThreadRun();
// 第三次登录失败
newThreadRun();
// 第四次登录失败
newThreadRun();
log.info("休眠20s开始");
Thread.sleep(20000);
log.info("休眠20s结束");
// 第五次登录成功
newThreadRunTrue();
log.info("模拟登录结束");
}
}
运行loginTest()方法中的逻辑,就能模拟实现账密错误多次账号锁定的逻辑。
运行打印记录如下:
2.实现类方法
为了便于维护和扩展,实际使用的时候最好是把初始化的逻辑放到实现类中,方便代码调用。
IGuavaCacheService接口类:
package com.dd.pp.user.auth.server.service;
import com.google.common.cache.LoadingCache;
/**
* @Author:
* @Date:2024/8/30 18:18
*/
public interface IGuavaCacheService {
LoadingCache<String, Integer> userLockService();
}
GuavaCacheServiceImpl实现类:
package com.dd.pp.user.auth.server.service.impl;
import com.dd.pp.user.auth.server.service.IGuavaCacheService;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @Author:
* @Date:2024/8/30 18:20
*/
@Service
@Slf4j
public class GuavaCacheServiceImpl implements IGuavaCacheService {
//私有化构造函数以防止外部调用实例化
private GuavaCacheServiceImpl() {}
/*
* 缓存初始化
* 实例过期时间5分钟,最大缓存数量1000
**/
private static final LoadingCache<String, Integer> LOADING_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(300, TimeUnit.SECONDS)
.maximumSize(1000)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
// 初始化时间为0,表示用户未被锁定
return 0;
}
});
@Override
public synchronized LoadingCache<String, Integer> userLockService() {
return LOADING_CACHE;
}
}
使用时,注入IGuavaCacheService ,然后调用类的userLockService()方法,即可获得一个缓存实例。
总结
本文主要讨论了Google GuavaCache将本地数据缓存到JVM内存的实现方法,通过本地缓存数据的方式也能轻松修复账密暴力破解漏洞。