Spring Security基于IP的访问控制与黑名单配置指南
作者:知远漫谈
引言
在现代 Web 应用程序中,安全性是至关重要的组成部分。随着互联网攻击手段的不断演进,保护系统资源、防止恶意请求和未授权访问已成为开发者的首要任务之一。Spring Security 作为 Java 生态中最主流的安全框架,提供了强大而灵活的身份认证(Authentication)和授权(Authorization)机制。其中,基于 IP 地址的访问控制是一种简单但非常有效的安全策略,尤其适用于防御暴力 破解、爬虫滥用、DDoS 攻击等场景。
本文将深入探讨如何使用 Spring Security 实现基于 IP 的访问控制与黑名单管理,涵盖从基础配置到高级自定义实现的全过程,并结合实际代码示例展示如何构建一个可扩展、高性能的 IP 控制体系。同时,我们将引入 mermaid 图表来帮助理解架构设计与流程逻辑,确保你不仅能“照着做”,更能“懂原理”。
为什么需要基于 IP 的访问控制?
在传统的用户身份验证模型中,系统通常依赖用户名/密码或 Token 进行认证。然而,在某些情况下,攻击者可能绕过这些机制,例如:
- 暴力尝试登录接口(如
/login) - 频繁调用公开 API 接口造成服务压力
- 使用代理池发起分布式请求进行数据抓取
- 利用已知漏洞反复探测敏感路径
此时,仅靠用户级别的权限控制已经不足以应对风险。引入 IP 层面的访问控制 可以提供一道额外防线,有效识别并阻止来自可疑来源的请求。
根据 OWASP Top Ten 的建议,输入验证和访问控制是防范常见安全威胁的关键措施之一。IP 黑名单正是访问控制的一种具体实践。
Spring Security 简介与核心组件回顾
Spring Security 是一个功能全面的安全框架,它通过过滤器链(Filter Chain)拦截 HTTP 请求,执行一系列安全检查。其核心组件包括:
SecurityFilterChain:定义安全过滤器的顺序和规则。AuthenticationManager:处理认证逻辑。AccessDecisionManager:决定是否允许访问受保护资源。HttpServletRequest:可用于获取客户端 IP 地址。
我们将在这些组件的基础上,扩展出一套完整的基于 IP 的访问控制系统。
架构设计:IP 访问控制的整体思路
要实现基于 IP 的访问控制,我们需要考虑以下几个关键点:
- 如何获取客户端真实 IP 地址?
- 如何判断该 IP 是否在黑名单中?
- 如何动态管理黑名单(增删改查)?
- 如何与其他安全机制协同工作?
- 如何保证性能,避免每次请求都查询数据库?
我们可以采用如下架构:

这个流程展示了典型的“缓存先行 + 后端持久化”模式,能够显著提升响应速度并降低数据库负载。
获取客户端真实 IP 地址
由于现代应用常部署在反向代理(如 Nginx、Cloudflare)之后,直接使用 request.getRemoteAddr() 可能只能获取到代理服务器的 IP。因此,我们必须优先读取标准的 HTTP 头字段,如:
X-Forwarded-ForX-Real-IPX-Forwarded-Host
下面是一个工具类,用于安全地提取客户端真实 IP:
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
public class IpAddressUtil {
public static String getClientIpAddress(HttpServletRequest request) {
// 优先从 X-Forwarded-For 中获取
String xff = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(xff)) {
// X-Forwarded-For 可能包含多个 IP,第一个为原始客户端
return xff.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP");
if (StringUtils.hasText(realIp)) {
return realIp.trim();
}
// 最后回退到远程地址
return request.getRemoteAddr();
}
}
注意:X-Forwarded-For 头可以被伪造,因此在生产环境中应配合可信代理白名单使用,或仅在内部网络中信任这些头信息。
自定义过滤器实现 IP 黑名单拦截
Spring Security 允许我们在过滤器链中插入自定义逻辑。我们将创建一个 IpBlockingFilter,用于在认证之前检查 IP 是否被禁止。
步骤一:定义黑名单存储接口
首先定义一个通用接口,便于后续切换不同实现(内存、数据库、Redis):
import java.util.Set;
public interface IpBlacklistService {
boolean isBlocked(String ipAddress);
void blockIp(String ipAddress);
void unblockIp(String ipAddress);
Set<String> getAllBlockedIps();
}
步骤二:内存实现(适用于测试)
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class InMemoryIpBlacklistService implements IpBlacklistService {
private final Set<String> blockedIps = Collections.newSetFromMap(new ConcurrentHashMap<>());
@Override
public boolean isBlocked(String ipAddress) {
return blockedIps.contains(ipAddress);
}
@Override
public void blockIp(String ipAddress) {
blockedIps.add(ipAddress);
}
@Override
public void unblockIp(String ipAddress) {
blockedIps.remove(ipAddress);
}
@Override
public Set<String> getAllBlockedIps() {
return new HashSet<>(blockedIps);
}
}
步骤三:创建自定义过滤器
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class IpBlockingFilter extends OncePerRequestFilter {
private final IpBlacklistService ipBlacklistService;
// 定义不需要拦截的路径(如静态资源)
private final AntPathRequestMatcher allowedRequestMatcher = new AntPathRequestMatcher("/public/**");
public IpBlockingFilter(IpBlacklistService ipBlacklistService) {
this.ipBlacklistService = ipBlacklistService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 跳过特定路径
if (allowedRequestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String clientIp = IpAddressUtil.getClientIpAddress(request);
if (ipBlacklistService.isBlocked(clientIp)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\": \"Access denied by IP blacklist\"}");
response.setContentType("application/json;charset=UTF-8");
return;
}
filterChain.doFilter(request, response);
}
}
使用 OncePerRequestFilter 可确保每个请求只执行一次,避免重复过滤。
将自定义过滤器注册进 Spring Security
接下来我们需要将 IpBlockingFilter 添加到 Spring Security 的过滤器链中。推荐将其放在 UsernamePasswordAuthenticationFilter 之前,以便尽早拦截非法请求。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final IpBlacklistService ipBlacklistService;
public SecurityConfig(IpBlacklistService ipBlacklistService) {
this.ipBlacklistService = ipBlacklistService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout.permitAll())
// 插入我们的 IP 拦截过滤器
.addFilterBefore(new IpBlockingFilter(ipBlacklistService),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
这样,所有进入系统的请求都会先经过 IP 黑名单检查,若命中则直接返回 403 错误。
数据库存储黑名单(JPA 实现)
内存存储适合开发调试,但在集群环境下无法共享状态。更稳健的做法是将黑名单持久化到数据库。
实体类定义
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "ip_blacklist")
public class BlockedIpEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String ipAddress;
@Column
private String reason;
@Column(nullable = false)
private LocalDateTime blockedAt;
// 构造函数
public BlockedIpEntity() {
this.blockedAt = LocalDateTime.now();
}
public BlockedIpEntity(String ipAddress, String reason) {
this();
this.ipAddress = ipAddress;
this.reason = reason;
}
// getter 和 setter 省略...
}
Repository 接口
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BlockedIpRepository extends JpaRepository<BlockedIpEntity, Long> {
boolean existsByIpAddress(String ipAddress);
}
数据库实现的服务层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class DatabaseIpBlacklistService implements IpBlacklistService {
@Autowired
private BlockedIpRepository repository;
@Override
public boolean isBlocked(String ipAddress) {
return repository.existsByIpAddress(ipAddress);
}
@Override
public void blockIp(String ipAddress) {
if (!isBlocked(ipAddress)) {
BlockedIpEntity entity = new BlockedIpEntity(ipAddress, "Manual block");
repository.save(entity);
}
}
@Override
public void unblockIp(String ipAddress) {
repository.deleteByIpAddress(ipAddress);
}
@Override
public Set<String> getAllBlockedIps() {
List<BlockedIpEntity> all = repository.findAll();
return all.stream()
.map(BlockedIpEntity::getIpAddress)
.collect(Collectors.toCollection(HashSet::new));
}
}
注意:这里假设你的项目已正确配置了 Spring Data JPA 和数据库连接。你可以参考 Spring Data JPA 官方文档 来完成初始化设置。
性能优化:引入缓存机制(Caffeine)
频繁查询数据库会影响性能。我们可以使用本地缓存(如 Caffeine)来减少数据库访问次数。
添加依赖(Maven)
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>缓存增强版服务
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CachedDatabaseIpBlacklistService implements IpBlacklistService {
private final DatabaseIpBlacklistService databaseService;
private final Cache<String, Boolean> cache;
public CachedDatabaseIpBlacklistService(DatabaseIpBlacklistService databaseService) {
this.databaseService = databaseService;
this.cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
}
@Override
public boolean isBlocked(String ipAddress) {
return cache.get(ipAddress, key -> databaseService.isBlocked(key));
}
@Override
public void blockIp(String ipAddress) {
databaseService.blockIp(ipAddress);
cache.put(ipAddress, true); // 显式放入缓存
}
@Override
public void unblockIp(String ipAddress) {
databaseService.unblockIp(ipAddress);
cache.invalidate(ipAddress);
}
@Override
public Set<String> getAllBlockedIps() {
return databaseService.getAllBlockedIps();
}
}
使用缓存后,99% 的查询将在毫秒内完成,极大提升了系统吞吐量。
动态管理黑名单:REST API 接口
为了方便运维人员操作,我们可以暴露一组 REST 接口用于管理黑名单。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Set;
@RestController
@RequestMapping("/api/admin/ip-blacklist")
public class IpBlacklistController {
@Autowired
private IpBlacklistService ipBlacklistService;
@GetMapping
public ResponseEntity<Set<String>> getBlockedIps() {
return ResponseEntity.ok(ipBlacklistService.getAllBlockedIps());
}
@PostMapping("/block")
public ResponseEntity<String> blockIp(@RequestParam String ip) {
ipBlacklistService.blockIp(ip);
return ResponseEntity.ok("IP blocked: " + ip);
}
@PostMapping("/unblock")
public ResponseEntity<String> unblockIp(@RequestParam String ip) {
ipBlacklistService.unblockIp(ip);
return ResponseEntity.ok("IP unblocked: " + ip);
}
}
配合前端界面或命令行工具,即可实现可视化管理。
分布式环境下的挑战与解决方案
在微服务或多实例部署中,单机内存缓存不再适用。此时可考虑以下方案:
方案一:使用 Redis 作为共享黑名单存储
Redis 提供高性能的键值存储,支持过期时间,非常适合用于 IP 黑名单。
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Set;
@Service
public class RedisIpBlacklistService implements IpBlacklistService {
private final StringRedisTemplate redisTemplate;
private static final String BLACKLIST_PREFIX = "blacklist:ip:";
public RedisIpBlacklistService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean isBlocked(String ipAddress) {
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + ipAddress));
}
@Override
public void blockIp(String ipAddress) {
redisTemplate.opsForValue().set(BLACKLIST_PREFIX + ipAddress, "1", Duration.ofDays(7));
}
@Override
public void unblockIp(String ipAddress) {
redisTemplate.delete(BLACKLIST_PREFIX + ipAddress);
}
@Override
public Set<String> getAllBlockedIps() {
// 实际项目中可通过 SCAN 命令遍历所有键
throw new UnsupportedOperationException("SCAN not implemented for large datasets");
}
}
方案二:结合消息队列实现跨节点同步
当使用内存存储时,可通过 Kafka 或 RabbitMQ 广播“IP 封禁”事件,通知其他节点更新本地缓存。

这种方式适合对实时性要求较高的系统。
基于频率的自动封禁机制(IP 限流)
除了手动添加黑名单外,我们还可以根据行为特征自动封禁异常 IP。例如:同一 IP 在 1 分钟内尝试登录失败超过 5 次,则自动加入黑名单。
自定义监听器捕获失败登录事件
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.stereotype.Component;
@Component
public class LoginFailureEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
private final IpLoginAttemptService attemptService;
public LoginFailureEventListener(IpLoginAttemptService attemptService) {
this.attemptService = attemptService;
}
@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
var details = (org.springframework.security.web.authentication.WebAuthenticationDetails) event.getAuthentication().getDetails();
String ip = details.getRemoteAddress();
attemptService.recordFailure(ip);
if (attemptService.getFailures(ip) >= 5) {
attemptService.blockIp(ip);
System.out.println("Automatically blocked IP due to repeated failures: " + ip);
}
}
}
登录尝试记录服务(基于 Caffeine)
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class IpLoginAttemptService {
private final Cache<String, Integer> loginFailures;
private final IpBlacklistService ipBlacklistService;
public IpLoginAttemptService(IpBlacklistService ipBlacklistService) {
this.ipBlacklistService = ipBlacklistService;
this.loginFailures = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
public void recordFailure(String ip) {
loginFailures.asMap().merge(ip, 1, Integer::sum);
}
public int getFailures(String ip) {
return loginFailures.getIfPresent(ip) != null ? loginFailures.getIfPresent(ip) : 0;
}
public void blockIp(String ip) {
ipBlacklistService.blockIp(ip);
loginFailures.invalidate(ip); // 清除计数
}
public void clearFailures(String ip) {
loginFailures.invalidate(ip);
}
}
成功登录后也应调用 clearFailures(ip) 以重置计数。
单元测试:验证 IP 过滤器行为
良好的测试是保障系统稳定的基石。下面我们编写一个简单的单元测试来验证 IpBlockingFilter 的功能。
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class IpBlockingFilterTest {
private IpBlacklistService ipBlacklistService;
private IpBlockingFilter filter;
@BeforeEach
void setUp() {
ipBlacklistService = mock(IpBlacklistService.class);
filter = new IpBlockingFilter(ipBlacklistService);
}
@Test
void shouldBlockWhenIpIsInBlacklist() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
request.setRemoteAddr("192.168.1.100");
when(ipBlacklistService.isBlocked("192.168.1.100")).thenReturn(true);
filter.doFilter(request, response, chain);
assertThat(response.getStatus()).isEqualTo(403);
assertThat(chain.getRequest()).isNull(); // 表示未继续传递
}
@Test
void shouldAllowWhenIpNotInBlacklist() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
request.setRemoteAddr("192.168.1.101");
when(ipBlacklistService.isBlocked("192.168.1.101")).thenReturn(false);
filter.doFilter(request, response, chain);
assertThat(response.getStatus()).isNotEqualTo(403);
assertThat(chain.getRequest()).isNotNull();
}
}
使用 spring-boot-starter-test 可轻松运行上述测试。
高级技巧:与 Spring Cloud Gateway 结合
如果你的应用使用了 Spring Cloud Gateway 作为统一入口网关,那么更适合将 IP 黑名单逻辑前置到网关层,避免流量到达下游服务。
示例:自定义 GlobalFilter
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class GatewayIpBlockingFilter implements GlobalFilter, Ordered {
private final IpBlacklistService ipBlacklistService;
public GatewayIpBlockingFilter(IpBlacklistService ipBlacklistService) {
this.ipBlacklistService = ipBlacklistService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String ip = IpAddressUtil.getClientIpAddress(exchange.getRequest());
if (ipBlacklistService.isBlocked(ip)) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100; // 非常高的优先级
}
}
最佳实践总结
| 实践 | 说明 |
|---|---|
| ✅ 优先使用可信代理头 | 避免 X-Forwarded-For 被伪造 |
| ✅ 使用缓存减少数据库压力 | 提升响应速度 |
| ✅ 支持动态更新 | 无需重启即可生效 |
| ✅ 日志记录封禁行为 | 便于审计与排查 |
| ✅ 设置自动解封机制 | 如 TTL 过期自动移除 |
| ✅ 结合限流与行为分析 | 实现智能风控 |
安全监控建议
仅仅封禁 IP 并不足够,还应建立完善的监控体系:
- 记录被拦截的 IP 和时间
- 统计高频攻击源地域分布
- 设置告警规则(如每分钟超过 100 次拦截触发邮件通知)
- 可视化仪表盘展示趋势
推荐结合 ELK(Elasticsearch + Logstash + Kibana)或 Prometheus + Grafana 实现日志聚合与可视化。
替代方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 内存存储 | 快速、无依赖 | 不支持集群 | 单机测试 |
| 数据库存储 | 持久化、可审计 | 查询慢 | 中小规模 |
| Redis 存储 | 高性能、支持 TTL | 需维护中间件 | 分布式系统 |
| CDN 层拦截 | 减轻源站压力 | 配置复杂 | 高流量网站 |
选择哪种方案取决于你的业务规模与架构复杂度。
结语
通过本文的学习,你应该已经掌握了如何在 Spring Security 中实现基于 IP 的访问控制与黑名单管理。无论是简单的内存存储,还是复杂的分布式缓存方案,都可以根据实际需求灵活选择。
记住:安全不是一次性工程,而是一个持续改进的过程。定期审查黑名单、分析攻击模式、升级防护策略,才能真正构筑坚固的防线。
“最好的防御,是让攻击者根本不知道你的存在。” —— 但这并不妨碍我们为可能出现的风险做好万全准备。
现在,就动手为你自己的系统加上这道“隐形防火墙”吧!
以上就是Spring Security基于IP的访问控制与黑名单配置指南的详细内容,更多关于Spring Security IP访问控制与黑名单配置的资料请关注脚本之家其它相关文章!
