文章目录
- 前言
- 一、暴力破解漏洞描述
- 二、Guava cache简介
- 三、GuavaCache本地缓存实现
- 1.测试实现方法
- 2.实现类方法
- 总结
前言
在登录模块出现暴力碰撞测试的安全漏洞时,我们最常见的方案就是需要记录账密错误,当达到一定错误阈值(比如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>() {@Overridepublic Integer load(String key) throws Exception {// 初始化时间为0,表示用户未被锁定return 0;}});}/*** 验证用户密码是否正确,如果错误,查询缓存中最新错误次数,并执行+1操作后更新缓存数据* @param userName 用户名* @param password 密码* @param lockDurationMinutes 锁定时间(分钟)* @return 是否锁定用户*/@Synchronizedpublic 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);}// 模拟输入错误帐密的登录请求@Testpublic 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("用户已锁定,请稍后再试");}}}// 模拟输入正确帐密的登录请求@Testpublic 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() {@Overridepublic 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() {@Overridepublic 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之后缓存失效,用户自动解锁再输入正确账密登录一次*/@Testvoid 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>() {@Overridepublic Integer load(String key) throws Exception {// 初始化时间为0,表示用户未被锁定return 0;}});@Overridepublic synchronized LoadingCache<String, Integer> userLockService() {return LOADING_CACHE;}
}
使用时,注入IGuavaCacheService ,然后调用类的userLockService()方法,即可获得一个缓存实例。
总结
本文主要讨论了Google GuavaCache将本地数据缓存到JVM内存的实现方法,通过本地缓存数据的方式也能轻松修复账密暴力破解漏洞。