Google GuavaCache将本地数据缓存到JVM内存,轻松修复账密暴力破解漏洞

13 篇文章 0 订阅
1 篇文章 0 订阅


前言

在登录模块出现暴力碰撞测试的安全漏洞时,我们最常见的方案就是需要记录账密错误,当达到一定错误阈值(比如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内存的实现方法,通过本地缓存数据的方式也能轻松修复账密暴力破解漏洞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值