黑白名单实现方案:让访问控制精准高效!🚫

63 阅读8分钟

标题: 黑白名单还在用数据库?Redis+布隆过滤器来了!
副标题: 从IP封禁到用户拉黑,打造高性能访问控制


🎬 开篇:一次恶意攻击的防御战

某API平台遭受恶意攻击:

攻击者:用脚本疯狂刷接口
QPS:从100 -> 10000 💀
服务器:CPU 100%
数据库:查询变慢
正常用户:无法访问 😭

紧急处理:
运营:这个IP一直在刷,能不能封掉?
开发:好的(加入黑名单)
数据库:每次请求都查询黑名单表...💀
性能:更慢了!

改用Redis后:
- 黑名单检查:O(1)时间复杂度 ⚡
- 响应时间:从50ms -> 1ms 🚀
- 自动过期:24小时后自动解封
- 攻击者:被完美拦截 ✅

效果:
- 恶意流量:拦截100%
- 正常用户:不受影响
- 服务器:恢复正常
- 成本:降低90%

教训:黑白名单要用高性能缓存!

🤔 什么是黑白名单?

想象一下:

  • 黑名单: 恶意IP、违规用户、刷单账号
  • 白名单: VIP用户、内部IP、合作伙伴
  • 灰名单: 可疑用户、需要验证的行为

黑白名单 = 访问控制 + 风险防控 + 高性能查询!


📚 知识地图

黑白名单系统
├── 🎯 应用场景
│   ├── IP封禁(黑名单)
│   ├── 用户拉黑(黑名单)
│   ├── VIP特权(白名单)
│   ├── 接口限流(黑白名单)
│   └── 内容审核(敏感词)
├── 💾 存储方案
│   ├── Redis Set(推荐)⭐⭐⭐⭐⭐
│   ├── 布隆过滤器(海量数据)⭐⭐⭐⭐⭐
│   ├── MySQL(持久化)⭐⭐⭐
│   └── Guava Cache(本地缓存)⭐⭐⭐
├── ⚡ 核心功能
│   ├── 添加/删除
│   ├── 快速查询
│   ├── 自动过期
│   ├── 批量导入
│   └── 命中统计
└── 📊 高级功能
    ├── 动态更新
    ├── 等级分类
    ├── 临时封禁
    └── 白名单优先

⚡ 方案1:Redis Set(推荐)

1. IP黑名单实现

/**
 * IP黑名单服务
 */
@Service
@Slf4j
public class IpBlacklistService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 黑名单Key
     */
    private static final String BLACKLIST_KEY = "blacklist:ip";
    
    /**
     * 白名单Key
     */
    private static final String WHITELIST_KEY = "whitelist:ip";
    
    /**
     * ⚡ 添加IP到黑名单
     */
    public void addToBlacklist(String ip, Long duration, TimeUnit unit) {
        // 添加到黑名单Set
        redisTemplate.opsForSet().add(BLACKLIST_KEY, ip);
        
        // 设置过期时间(单独为这个IP设置)
        if (duration != null && duration > 0) {
            String tempKey = BLACKLIST_KEY + ":temp:" + ip;
            redisTemplate.opsForValue().set(tempKey, "1", duration, unit);
        }
        
        log.info("IP加入黑名单:ip={}, duration={}{}",
            ip, duration, unit);
    }
    
    /**
     * ⚡ 从黑名单移除IP
     */
    public void removeFromBlacklist(String ip) {
        redisTemplate.opsForSet().remove(BLACKLIST_KEY, ip);
        
        // 删除临时Key
        String tempKey = BLACKLIST_KEY + ":temp:" + ip;
        redisTemplate.delete(tempKey);
        
        log.info("IP从黑名单移除:ip={}", ip);
    }
    
    /**
     * ⚡ 检查IP是否在黑名单(O(1)复杂度)
     */
    public boolean isBlacklisted(String ip) {
        // 先检查白名单(白名单优先)
        if (isWhitelisted(ip)) {
            return false;
        }
        
        // 检查黑名单
        Boolean result = redisTemplate.opsForSet().isMember(BLACKLIST_KEY, ip);
        
        if (result != null && result) {
            // 记录命中次数
            incrementHitCount(ip, "blacklist");
            return true;
        }
        
        return false;
    }
    
    /**
     * ⚡ 添加IP到白名单
     */
    public void addToWhitelist(String ip) {
        redisTemplate.opsForSet().add(WHITELIST_KEY, ip);
        log.info("IP加入白名单:ip={}", ip);
    }
    
    /**
     * 从白名单移除IP
     */
    public void removeFromWhitelist(String ip) {
        redisTemplate.opsForSet().remove(WHITELIST_KEY, ip);
        log.info("IP从白名单移除:ip={}", ip);
    }
    
    /**
     * 检查IP是否在白名单
     */
    public boolean isWhitelisted(String ip) {
        Boolean result = redisTemplate.opsForSet().isMember(WHITELIST_KEY, ip);
        return result != null && result;
    }
    
    /**
     * ⚡ 批量添加黑名单
     */
    public void batchAddToBlacklist(List<String> ips) {
        if (ips == null || ips.isEmpty()) {
            return;
        }
        
        redisTemplate.opsForSet().add(BLACKLIST_KEY, ips.toArray(new String[0]));
        
        log.info("批量添加黑名单:count={}", ips.size());
    }
    
    /**
     * 获取黑名单列表
     */
    public Set<String> getBlacklist() {
        return redisTemplate.opsForSet().members(BLACKLIST_KEY);
    }
    
    /**
     * 获取白名单列表
     */
    public Set<String> getWhitelist() {
        return redisTemplate.opsForSet().members(WHITELIST_KEY);
    }
    
    /**
     * 黑名单数量
     */
    public Long getBlacklistSize() {
        return redisTemplate.opsForSet().size(BLACKLIST_KEY);
    }
    
    /**
     * ⚡ 记录命中次数
     */
    private void incrementHitCount(String ip, String type) {
        String key = "blacklist:hit:" + type + ":" + 
            LocalDate.now().toString();
        
        redisTemplate.opsForHash().increment(key, ip, 1);
        
        // 设置过期时间(保留7天)
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
    }
    
    /**
     * 获取IP命中次数
     */
    public Long getHitCount(String ip, String type) {
        String key = "blacklist:hit:" + type + ":" + 
            LocalDate.now().toString();
        
        Object count = redisTemplate.opsForHash().get(key, ip);
        return count != null ? Long.parseLong(count.toString()) : 0L;
    }
}

2. 用户黑名单实现

/**
 * 用户黑名单服务
 */
@Service
@Slf4j
public class UserBlacklistService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String USER_BLACKLIST_KEY = "blacklist:user";
    
    /**
     * ⚡ 拉黑用户
     */
    public void blockUser(Long userId, String reason, Long duration, TimeUnit unit) {
        // 1. 添加到黑名单
        redisTemplate.opsForSet().add(USER_BLACKLIST_KEY, String.valueOf(userId));
        
        // 2. 记录拉黑原因和时间
        UserBlacklistInfo info = new UserBlacklistInfo();
        info.setUserId(userId);
        info.setReason(reason);
        info.setBlockTime(new Date());
        
        if (duration != null && duration > 0) {
            info.setExpireTime(new Date(System.currentTimeMillis() + 
                unit.toMillis(duration)));
        }
        
        String infoKey = "blacklist:user:info:" + userId;
        redisTemplate.opsForValue().set(infoKey, JSON.toJSONString(info),
            duration != null ? duration : 0, 
            duration != null ? unit : TimeUnit.SECONDS);
        
        log.info("用户被拉黑:userId={}, reason={}, duration={}{}",
            userId, reason, duration, unit);
    }
    
    /**
     * ⚡ 解除拉黑
     */
    public void unblockUser(Long userId) {
        redisTemplate.opsForSet().remove(USER_BLACKLIST_KEY, String.valueOf(userId));
        
        String infoKey = "blacklist:user:info:" + userId;
        redisTemplate.delete(infoKey);
        
        log.info("用户解除拉黑:userId={}", userId);
    }
    
    /**
     * ⚡ 检查用户是否被拉黑
     */
    public boolean isBlocked(Long userId) {
        Boolean result = redisTemplate.opsForSet().isMember(
            USER_BLACKLIST_KEY, String.valueOf(userId));
        
        return result != null && result;
    }
    
    /**
     * 获取拉黑信息
     */
    public UserBlacklistInfo getBlacklistInfo(Long userId) {
        String infoKey = "blacklist:user:info:" + userId;
        String info = redisTemplate.opsForValue().get(infoKey);
        
        if (StringUtils.isNotBlank(info)) {
            return JSON.parseObject(info, UserBlacklistInfo.class);
        }
        
        return null;
    }
}

/**
 * 用户黑名单信息
 */
@Data
public class UserBlacklistInfo {
    
    private Long userId;
    private String reason;
    private Date blockTime;
    private Date expireTime;
}

🎯 方案2:布隆过滤器(海量数据)

/**
 * 布隆过滤器黑名单(适用于海量IP)
 */
@Service
@Slf4j
public class BloomFilterBlacklistService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    private RBloomFilter<String> ipBlacklistFilter;
    
    /**
     * 初始化布隆过滤器
     */
    @PostConstruct
    public void init() {
        // ⚡ 创建布隆过滤器
        ipBlacklistFilter = redissonClient.getBloomFilter("blacklist:ip:bloom");
        
        // 初始化(预期元素数量:1000万,误判率:0.01%)
        ipBlacklistFilter.tryInit(10_000_000L, 0.0001);
        
        log.info("布隆过滤器初始化完成");
    }
    
    /**
     * ⚡ 添加IP到黑名单
     */
    public void addToBlacklist(String ip) {
        ipBlacklistFilter.add(ip);
        log.info("IP加入布隆过滤器黑名单:ip={}", ip);
    }
    
    /**
     * ⚡ 检查IP是否可能在黑名单(可能有误判)
     */
    public boolean mightContain(String ip) {
        return ipBlacklistFilter.contains(ip);
    }
    
    /**
     * ⚡ 精确检查(布隆过滤器+Redis Set双重验证)
     */
    public boolean isBlacklisted(String ip) {
        // 1. 先用布隆过滤器快速过滤(绝对不存在直接返回)
        if (!ipBlacklistFilter.contains(ip)) {
            return false;
        }
        
        // 2. 可能存在,再用Redis Set精确验证
        Boolean result = redisTemplate.opsForSet().isMember("blacklist:ip:set", ip);
        
        return result != null && result;
    }
    
    /**
     * 获取布隆过滤器统计信息
     */
    public BloomFilterStats getStats() {
        BloomFilterStats stats = new BloomFilterStats();
        stats.setExpectedInsertions(ipBlacklistFilter.getExpectedInsertions());
        stats.setSize(ipBlacklistFilter.getSize());
        stats.setHashIterations(ipBlacklistFilter.getHashIterations());
        
        return stats;
    }
}

/**
 * 布隆过滤器统计信息
 */
@Data
public class BloomFilterStats {
    private Long expectedInsertions;
    private Long size;
    private Integer hashIterations;
}

🔧 拦截器实现

1. IP黑名单拦截器

/**
 * IP黑名单拦截器
 */
@Component
@Slf4j
public class IpBlacklistInterceptor implements HandlerInterceptor {
    
    @Autowired
    private IpBlacklistService blacklistService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response,
                            Object handler) throws Exception {
        
        // 1. 获取客户端IP
        String ip = getClientIp(request);
        
        // 2. ⚡ 检查是否在黑名单
        if (blacklistService.isBlacklisted(ip)) {
            log.warn("IP在黑名单中,拒绝访问:ip={}", ip);
            
            // 返回403
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json;charset=UTF-8");
            
            Result<?> result = Result.fail("您的IP已被封禁,请联系管理员");
            response.getWriter().write(JSON.toJSONString(result));
            
            return false;
        }
        
        return true;
    }
    
    /**
     * 获取客户端真实IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        
        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        
        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        
        // 如果是多级代理,取第一个IP
        if (StringUtils.isNotBlank(ip) && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        
        return ip;
    }
}

/**
 * 注册拦截器
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private IpBlacklistInterceptor ipBlacklistInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ipBlacklistInterceptor)
            .addPathPatterns("/**")
            .excludePathPatterns("/health", "/actuator/**");
    }
}

2. 用户黑名单拦截器

/**
 * 用户黑名单拦截器
 */
@Aspect
@Component
@Slf4j
public class UserBlacklistAspect {
    
    @Autowired
    private UserBlacklistService blacklistService;
    
    /**
     * ⚡ 拦截需要登录的接口
     */
    @Before("@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void checkUserBlacklist() {
        // 获取当前用户
        Long userId = UserContextHolder.getUserId();
        
        if (userId == null) {
            return;
        }
        
        // ⚡ 检查是否被拉黑
        if (blacklistService.isBlocked(userId)) {
            UserBlacklistInfo info = blacklistService.getBlacklistInfo(userId);
            
            String message = "您的账号已被封禁";
            if (info != null && StringUtils.isNotBlank(info.getReason())) {
                message += ",原因:" + info.getReason();
            }
            
            log.warn("用户被拉黑,拒绝访问:userId={}", userId);
            
            throw new BusinessException(message);
        }
    }
}

📊 管理后台接口

/**
 * 黑名单管理Controller
 */
@RestController
@RequestMapping("/admin/blacklist")
@Slf4j
public class BlacklistController {
    
    @Autowired
    private IpBlacklistService ipBlacklistService;
    
    @Autowired
    private UserBlacklistService userBlacklistService;
    
    /**
     * ⚡ 添加IP黑名单
     */
    @PostMapping("/ip/add")
    public Result<Void> addIpBlacklist(@RequestBody IpBlacklistDTO dto) {
        ipBlacklistService.addToBlacklist(
            dto.getIp(), 
            dto.getDuration(), 
            dto.getUnit());
        
        return Result.success();
    }
    
    /**
     * 移除IP黑名单
     */
    @DeleteMapping("/ip/remove")
    public Result<Void> removeIpBlacklist(@RequestParam String ip) {
        ipBlacklistService.removeFromBlacklist(ip);
        return Result.success();
    }
    
    /**
     * 查询IP黑名单
     */
    @GetMapping("/ip/list")
    public Result<Set<String>> getIpBlacklist() {
        Set<String> blacklist = ipBlacklistService.getBlacklist();
        return Result.success(blacklist);
    }
    
    /**
     * 检查IP状态
     */
    @GetMapping("/ip/check")
    public Result<IpStatusVO> checkIp(@RequestParam String ip) {
        IpStatusVO status = new IpStatusVO();
        status.setIp(ip);
        status.setBlacklisted(ipBlacklistService.isBlacklisted(ip));
        status.setWhitelisted(ipBlacklistService.isWhitelisted(ip));
        status.setHitCount(ipBlacklistService.getHitCount(ip, "blacklist"));
        
        return Result.success(status);
    }
    
    /**
     * ⚡ 拉黑用户
     */
    @PostMapping("/user/block")
    public Result<Void> blockUser(@RequestBody UserBlockDTO dto) {
        userBlacklistService.blockUser(
            dto.getUserId(),
            dto.getReason(),
            dto.getDuration(),
            dto.getUnit());
        
        return Result.success();
    }
    
    /**
     * 解除拉黑
     */
    @PostMapping("/user/unblock")
    public Result<Void> unblockUser(@RequestParam Long userId) {
        userBlacklistService.unblockUser(userId);
        return Result.success();
    }
}

/**
 * IP状态VO
 */
@Data
public class IpStatusVO {
    private String ip;
    private Boolean blacklisted;
    private Boolean whitelisted;
    private Long hitCount;
}

⚡ 自动化管理

1. 定时清理过期黑名单

/**
 * 黑名单定时任务
 */
@Component
@Slf4j
public class BlacklistScheduleTask {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * ⚡ 每小时检查并清理过期的临时黑名单
     */
    @Scheduled(cron = "0 0 * * * ?")
    public void cleanExpiredBlacklist() {
        log.info("开始清理过期黑名单");
        
        String pattern = "blacklist:ip:temp:*";
        Set<String> keys = redisTemplate.keys(pattern);
        
        if (keys == null || keys.isEmpty()) {
            return;
        }
        
        int count = 0;
        for (String key : keys) {
            // 检查是否过期
            Long ttl = redisTemplate.getExpire(key);
            
            if (ttl != null && ttl <= 0) {
                // 从黑名单Set中移除
                String ip = key.replace("blacklist:ip:temp:", "");
                redisTemplate.opsForSet().remove("blacklist:ip", ip);
                redisTemplate.delete(key);
                count++;
            }
        }
        
        log.info("清理过期黑名单完成:count={}", count);
    }
}

2. 自动拉黑恶意IP

/**
 * 自动拉黑服务
 */
@Service
@Slf4j
public class AutoBlacklistService {
    
    @Autowired
    private IpBlacklistService blacklistService;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * ⚡ 记录IP访问频率
     */
    public void recordIpAccess(String ip) {
        String key = "ip:access:count:" + ip;
        
        Long count = redisTemplate.opsForValue().increment(key);
        
        if (count == 1) {
            // 设置过期时间(1分钟)
            redisTemplate.expire(key, 1, TimeUnit.MINUTES);
        }
        
        // ⚡ 如果1分钟内访问超过100次,自动拉黑
        if (count != null && count > 100) {
            blacklistService.addToBlacklist(ip, 24L, TimeUnit.HOURS);
            
            log.warn("IP访问频率过高,自动拉黑:ip={}, count={}", ip, count);
        }
    }
}

✅ 最佳实践

黑白名单系统最佳实践:

1️⃣ 存储选型:
   □ 小数据量(< 10万):Redis Set
   □ 大数据量(> 100万):布隆过滤器 + Redis Set
   □ 持久化:MySQL备份
   
2️⃣ 性能优化:
   □ 白名单优先(先检查白名单)
   □ 布隆过滤器快速过滤
   □ 本地缓存(Guava Cache)
   □ 批量操作
   
3️⃣ 功能设计:
   □ 临时封禁(自动过期)
   □ 等级分类(轻度/重度)
   □ 命中统计
   □ 操作日志
   
4️⃣ 安全防护:
   □ 获取真实IP(处理代理)
   □ 自动拉黑(异常行为)
   □ 人工审核
   □ 申诉机制
   
5️⃣ 监控告警:
   □ 黑名单数量监控
   □ 命中率统计
   □ 异常封禁告警
   □ 误判检测

🎉 总结

黑白名单系统核心:

1️⃣ Redis Set:O(1)查询,性能极佳
2️⃣ 布隆过滤器:海量数据,空间节省
3️⃣ 白名单优先:先检查白名单
4️⃣ 自动过期:临时封禁自动解除
5️⃣ 命中统计:实时监控拦截效果

记住:黑白名单是访问控制的第一道防线! 🚫


文档编写时间:2025年10月24日
作者:热爱安全防护的访问控制工程师
版本:v1.0
愿每次访问都安全可控!