ITPUB

凌晨三点接口被刷,我用SpringBoot动态IP白名单过滤掉了99%的非法请求!

凌晨三点,手机突然炸了——不是女神消息,而是运维小哥的夺命连环call:“大哥,咱接口被当公共厕所了!全是陌生IP在疯狂拉数据,数据库CPU都快烧了!”

你揉着惺忪的睡眼打开监控,看着那根像比特币K线一样飙升的QPS曲线,还有满屏404的异常日志,心里只有一个念头:这要是被老板知道了,下个月的奖金怕是要泡汤了!

别慌!今天我就带你解锁SpringBoot接口安全的第一道防线——IP白名单。从为什么需要它,到怎么实现,再到如何优雅地运维,保证让你看完就能直接上手,再也不怕半夜被call!

一、为什么IP白名单是你的"数字保镖"?

有些同学可能会问:"我都用了JWT、验证码、接口限流三件套了,还需要IP白名单吗?"

这好比你家装了防盗门(JWT)、门口站着保安(验证码)、还限制每次只能进一个人(限流)。但如果有一百个小偷轮流来撬锁,就算一次只进一个,也能把你家翻个底朝天!

IP白名单就是给小区大门加了个"人脸识别系统"——只有录入过的面孔才能进小区,坏人连靠近大门的机会都没有。

真实教训:之前一个同事负责一个银行支付回调接口,只对接银联和支付宝的官方IP。觉得有HTTPS+签名验证就够了,结果上线一周被爬虫刷了80万次!虽然没造成资金损失,但数据库连接池直接爆满,正常交易延迟飙升。加上IP白名单后,非法请求直接归零,运维小哥再也不给他发"问候"了。

记住:IP白名单不是要取代其他安全措施,而是在攻击到达你的核心业务前,先过滤掉99%的噪音。

二、青铜段位:静态IP白名单(5分钟搞定)

适合场景:内部管理系统、测试环境、固定合作伙伴。

2.1 核心原理:拦截器当门卫

SpringBoot的拦截器就像小区门口的保安亭,每个请求都要先到这里登记。我们给保安一份"业主名单",不是业主的直接劝返!

2.2 代码实战:手把手教你写门卫

第一步:创建IP拦截器(保安上岗)

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * IP白名单拦截器 - 你的专属数字门卫
 * 作者:防秃顶协会资深会员
 */

publicclassIpWhitelistInterceptorimplementsHandlerInterceptor{

// 业主名单(实际项目中建议放配置文件)
privatestaticfinal Set<String> WHITE_LIST = new HashSet<>(Arrays.asList(
"127.0.0.1",        // 本地开发
"192.168.1.100",    // 张三的电脑
"10.0.0.15",        // 测试服务器
"203.0.113.25"// 第三方合作伙伴
    ));

@Override
publicbooleanpreHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler)
throws Exception 
{

        String clientIp = getRealClientIp(request);
        System.out.println("门卫检查:IP=" + clientIp + " 试图访问=" + request.getRequestURI());

// 检查是否是业主
if (!WHITE_LIST.contains(clientIp)) {
            response.setStatus(403); // 403 Forbidden
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":403,\"msg\":\"IP不在白名单内,请联系管理员\"}");
returnfalse; // 不放行
        }

        System.out.println("门卫放行:" + clientIp);
returntrue; // 放行
    }

@Override
publicvoidpostHandle(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler, 
                         ModelAndView modelAndView)
throws Exception 
{
// 请求处理后调用(可以记录日志等)
    }

@Override
publicvoidafterCompletion(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler, 
                              Exception ex)
throws Exception 
{
// 请求完成后调用(可以清理资源等)
    }

/**
     * 获取真实客户端IP(重要!很多兄弟在这里栽跟头)
     * 如果你的项目前面有Nginx等反向代理,必须用这个方法
     */

private String getRealClientIp(HttpServletRequest request){
// 检查各种可能的请求头(优先级从高到低)
        String[] ipHeaders = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_CLIENT_IP"
        };

for (String header : ipHeaders) {
            String ip = request.getHeader(header);
if (ip != null && ip.length() > 0 && !"unknown".equalsIgnoreCase(ip)) {
// 如果包含多个IP(代理链),取第一个
if (ip.contains(",")) {
return ip.split(",")[0].trim();
                }
return ip.trim();
            }
        }

// 如果以上都没有,使用默认方法
return request.getRemoteAddr();
    }
}

第二步:注册拦截器(给保安分配岗位)

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
publicclassSecurityConfigimplementsWebMvcConfigurer{

@Override
publicvoidaddInterceptors(InterceptorRegistry registry){
// 门卫上岗,指定巡逻区域
        registry.addInterceptor(new IpWhitelistInterceptor())
                .addPathPatterns("/api/admin/**")    // 管理接口要检查
                .addPathPatterns("/api/payment/**")  // 支付接口要检查
                .addPathPatterns("/api/internal/**") // 内部接口要检查
                .excludePathPatterns("/api/public/**")  // 公共接口不检查
                .excludePathPatterns("/error")       // 错误页面不检查
                .excludePathPatterns("/health")      // 健康检查不检查
                .excludePathPatterns("/swagger-ui/**") // Swagger文档不检查
                .excludePathPatterns("/v3/api-docs/**"); // OpenAPI文档不检查
    }
}

第三步:Nginx配置(如果你用了反向代理)

# nginx.conf
server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://your-app:8080;

        # 关键配置:转发真实IP
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

第四步:测试效果

# 用白名单IP访问
curl -X GET "http://localhost:8080/api/admin/users"
# 返回:正常数据

# 用非白名单IP访问
curl -X GET "http://localhost:8080/api/admin/users"
# 返回:{"code":403,"msg":"IP不在白名单内,请联系管理员"}

三、黄金段位:动态IP白名单(热更新不重启)

静态白名单有个致命缺点:每次加个IP都要改代码、重启服务。生产环境能这么玩吗?显然不能!

解决方案:IP存数据库 + Redis缓存,通过管理界面随时增删改查,实时生效不重启!

3.1 技术架构

客户端请求
    ↓
拦截器(门卫)
    ↓
查询Redis缓存 ←→ 缓存命中 → 校验IP
    ↓ (缓存未命中)
查询数据库
    ↓
加载到缓存
    ↓
校验IP

3.2 完整实现

第一步:数据库表设计

-- 创建IP白名单表
CREATETABLE`sys_ip_whitelist` (
`id`BIGINTUNSIGNEDNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`ip_address`VARCHAR(50) NOTNULLCOMMENT'IP地址',
`description`VARCHAR(200) DEFAULTNULLCOMMENT'描述(如:财务部服务器)',
`status`TINYINTNOTNULLDEFAULT1COMMENT'状态:1-启用 0-禁用',
`created_by`VARCHAR(50) DEFAULTNULLCOMMENT'创建人',
`created_time` DATETIME NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`updated_time` DATETIME NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
    PRIMARY KEY (`id`),
UNIQUEKEY`uk_ip_address` (`ip_address`),
KEY`idx_status` (`status`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='系统IP白名单表';

第二步:Maven依赖

<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MyBatis Plus(数据库操作神器) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>

<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

第三步:配置文件

# application.yml
spring:
# 数据源配置
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username:root
password:123456
hikari:
maximum-pool-size:20
minimum-idle:5

# Redis配置
redis:
host:localhost
port:6379
password:
database:0
timeout:3000ms
lettuce:
pool:
max-active:8
max-idle:8
min-idle:0

# MyBatis Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case:true
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field:deleted# 逻辑删除字段
logic-delete-value:1# 逻辑已删除值
logic-not-delete-value:0# 逻辑未删除值

# 应用配置
server:
port:8080

第四步:实体类

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("sys_ip_whitelist")
publicclassIpWhitelist{

@TableId(type = IdType.AUTO)
private Long id;

@TableField("ip_address")
private String ipAddress;

private String description;

private Integer status; // 1-启用 0-禁用

@TableField("created_by")
private String createdBy;

@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;

@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

第五步:Mapper接口

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;

@Mapper
publicinterfaceIpWhitelistMapperextendsBaseMapper<IpWhitelist> {

/**
     * 查询所有启用的IP地址
     */

@Select("SELECT ip_address FROM sys_ip_whitelist WHERE status = 1")
List<String> selectActiveIpList();
}

第六步:Service层(核心缓存逻辑)

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
publicclassIpWhitelistServiceextendsServiceImpl<IpWhitelistMapper, IpWhitelist> {

// Redis缓存键
privatestaticfinal String CACHE_KEY = "sys:ip:whitelist:active";
// 缓存时间(分钟)
privatestaticfinallong CACHE_TTL = 30;

@Resource
private IpWhitelistMapper ipWhitelistMapper;

@Resource
private RedisTemplate<String, Object> redisTemplate;

/**
     * 获取所有启用的IP(带缓存)
     */

public List<String> getActiveIpList(){
// 1. 尝试从缓存获取
        List<String> cachedList = (List<String>) redisTemplate.opsForValue().get(CACHE_KEY);
if (cachedList != null) {
            log.debug("缓存命中,IP数量:{}", cachedList.size());
return cachedList;
        }

// 2. 缓存未命中,查询数据库
        log.info("缓存未命中,查询数据库...");
        List<String> dbList = ipWhitelistMapper.selectActiveIpList();

if (CollectionUtils.isEmpty(dbList)) {
            log.warn("数据库中没有启用的IP白名单!");
            dbList = List.of(); // 返回空列表,避免NPE
        }

// 3. 写入缓存(设置TTL)
        redisTemplate.opsForValue().set(
            CACHE_KEY, 
            dbList, 
            CACHE_TTL, 
            TimeUnit.MINUTES
        );

        log.info("已缓存IP白名单,数量:{}", dbList.size());
return dbList;
    }

/**
     * 检查IP是否在白名单中
     */

publicbooleanisIpAllowed(String ip){
if (ip == null || ip.trim().isEmpty()) {
returnfalse;
        }

        List<String> allowedIps = getActiveIpList();
boolean allowed = allowedIps.contains(ip.trim());

        log.debug("IP检查:{} -> {}", ip, allowed ? "允许" : "拒绝");
return allowed;
    }

/**
     * 刷新缓存(增删改后调用)
     */

publicvoidrefreshCache(){
        Boolean deleted = redisTemplate.delete(CACHE_KEY);
if (Boolean.TRUE.equals(deleted)) {
            log.info("缓存清除成功");
        } else {
            log.info("缓存不存在或清除失败");
        }

// 预热缓存
        getActiveIpList();
    }

/**
     * 新增IP并刷新缓存
     */

publicbooleanaddIp(String ip, String description, String operator){
// 检查IP格式
if (!isValidIp(ip)) {
thrownew IllegalArgumentException("IP地址格式无效: " + ip);
        }

// 检查是否已存在
long count = this.lambdaQuery()
                .eq(IpWhitelist::getIpAddress, ip)
                .count();

if (count > 0) {
thrownew IllegalStateException("IP已存在: " + ip);
        }

// 保存到数据库
        IpWhitelist entity = new IpWhitelist();
        entity.setIpAddress(ip);
        entity.setDescription(description);
        entity.setStatus(1);
        entity.setCreatedBy(operator);

boolean saved = this.save(entity);

if (saved) {
// 刷新缓存
            refreshCache();
            log.info("新增IP成功:{},操作人:{}", ip, operator);
        }

return saved;
    }

/**
     * IP地址格式验证
     */

privatebooleanisValidIp(String ip){
        String ipv4Pattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
return ip != null && ip.matches(ipv4Pattern);
    }
}

第七步:动态拦截器

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
publicclassDynamicIpInterceptorimplementsHandlerInterceptor{

@Resource
private IpWhitelistService ipWhitelistService;

@Override
publicbooleanpreHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler)
throws Exception 
{

        String clientIp = extractClientIp(request);
        String requestUri = request.getRequestURI();

// 跳过不需要检查的路径
if (shouldSkip(requestUri)) {
returntrue;
        }

// 检查IP是否在白名单中
if (!ipWhitelistService.isIpAllowed(clientIp)) {
            log.warn("非法IP访问被拦截:IP={}, URI={}", clientIp, requestUri);

            response.setStatus(403);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(String.format(
"{\"success\":false,\"code\":403,\"message\":\"IP [%s] 无访问权限\",\"timestamp\":%d}",
                clientIp, System.currentTimeMillis()
            ));

returnfalse;
        }

        log.debug("IP访问通过:{} -> {}", clientIp, requestUri);
returntrue;
    }

/**
     * 提取客户端真实IP
     */

private String extractClientIp(HttpServletRequest request){
        String[] headers = {
"X-Forwarded-For",
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_CLIENT_IP"
        };

for (String header : headers) {
            String ip = request.getHeader(header);
if (isValidIp(ip)) {
// 处理多个IP的情况
if (ip.contains(",")) {
return ip.split(",")[0].trim();
                }
return ip.trim();
            }
        }

return request.getRemoteAddr();
    }

/**
     * 判断是否有效的IP(非空且不是unknown)
     */

privatebooleanisValidIp(String ip){
return ip != null && 
               !ip.isEmpty() && 
               !"unknown".equalsIgnoreCase(ip) &&
               !ip.startsWith("0.");
    }

/**
     * 判断是否跳过IP检查
     */

privatebooleanshouldSkip(String uri){
// 公开接口、健康检查等不需要IP检查
return uri.startsWith("/api/public/") ||
               uri.equals("/health") ||
               uri.startsWith("/actuator/") ||
               uri.startsWith("/swagger-ui/") ||
               uri.startsWith("/v3/api-docs") ||
               uri.startsWith("/error") ||
               uri.startsWith("/favicon.ico");
    }
}

第八步:管理控制器(给运维同学用)

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.time.LocalDateTime;
import java.util.List;

@RestController
@RequestMapping("/api/system/ip-whitelist")
publicclassIpWhitelistController{

@Resource
private IpWhitelistService ipWhitelistService;

/**
     * 分页查询白名单
     */

@GetMapping("/page")
public ApiResponse<IPage<IpWhitelist>> page(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword) {

        Page<IpWhitelist> page = new Page<>(pageNum, pageSize);
        QueryWrapper<IpWhitelist> wrapper = new QueryWrapper<>();

if (keyword != null && !keyword.trim().isEmpty()) {
            wrapper.like("ip_address", keyword)
                   .or()
                   .like("description", keyword);
        }

        wrapper.orderByDesc("created_time");
        IPage<IpWhitelist> result = ipWhitelistService.page(page, wrapper);

return ApiResponse.success(result);
    }

/**
     * 新增IP
     */

@PostMapping
public ApiResponse<String> add(@Valid @RequestBody AddIpRequest request){
try {
boolean success = ipWhitelistService.addIp(
                request.getIpAddress(),
                request.getDescription(),
                request.getOperator()
            );

return success ? 
                ApiResponse.success("新增成功") : 
                ApiResponse.error("新增失败");

        } catch (IllegalArgumentException e) {
return ApiResponse.error(e.getMessage());
        } catch (IllegalStateException e) {
return ApiResponse.error(e.getMessage());
        } catch (Exception e) {
return ApiResponse.error("系统错误: " + e.getMessage());
        }
    }

/**
     * 删除IP
     */

@DeleteMapping("/{id}")
public ApiResponse<String> delete(@PathVariable Long id){
boolean removed = ipWhitelistService.removeById(id);
if (removed) {
            ipWhitelistService.refreshCache();
return ApiResponse.success("删除成功");
        }
return ApiResponse.error("删除失败,IP不存在");
    }

/**
     * 启用/禁用IP
     */

@PutMapping("/{id}/status")
public ApiResponse<String> updateStatus(
            @PathVariable Long id,
            @RequestParam Integer status)
{

if (status != 0 && status != 1) {
return ApiResponse.error("状态值无效(0=禁用, 1=启用)");
        }

        IpWhitelist entity = new IpWhitelist();
        entity.setId(id);
        entity.setStatus(status);
        entity.setUpdatedTime(LocalDateTime.now());

boolean updated = ipWhitelistService.updateById(entity);
if (updated) {
            ipWhitelistService.refreshCache();
return ApiResponse.success(status == 1 ? "已启用" : "已禁用");
        }

return ApiResponse.error("更新失败");
    }
}

/**
 * 新增IP请求体
 */

@Data
classAddIpRequest{
@NotBlank(message = "IP地址不能为空")
@Pattern(regexp = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
             message = "IP地址格式无效")
private String ipAddress;

private String description;

@NotBlank(message = "操作人不能为空")
private String operator;
}

/**
 * 统一API响应格式
 */

@Data
classApiResponse<T> {
privateboolean success;
private String message;
private T data;
privatelong timestamp;

privateApiResponse(boolean success, String message, T data){
this.success = success;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
    }

publicstatic <T> ApiResponse<T> success(T data){
returnnew ApiResponse<>(true, "成功", data);
    }

publicstatic <T> ApiResponse<T> success(String message, T data){
returnnew ApiResponse<>(true, message, data);
    }

publicstatic <T> ApiResponse<T> error(String message){
returnnew ApiResponse<>(false, message, null);
    }
}

第九步:注册动态拦截器

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
publicclassDynamicInterceptorConfigimplementsWebMvcConfigurer{

@Override
publicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(new DynamicIpInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**")
                .excludePathPatterns("/api/system/ip-whitelist/**") // 管理接口本身需要特殊处理
                .excludePathPatterns("/error", "/health", "/actuator/**");
    }
}

四、王者段位:Spring Security集成版

如果你的项目已经用了Spring Security,可以这样优雅集成:

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;
import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
publicclassSecurityConfig{

@Resource
private IpWhitelistService ipWhitelistService;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
        http
// 添加IP白名单过滤器
            .addFilterBefore(ipWhitelistFilter(), UsernamePasswordAuthenticationFilter.class)

            // 配置权限规则
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/system/ip-whitelist/**").hasRole("ADMIN") // 只有管理员能操作白名单
                .antMatchers("/api/admin/**").hasAnyRole("ADMIN", "MANAGER")
                .anyRequest().authenticated()
            .and()

            // 禁用CSRF(API服务通常禁用)
            .csrf().disable()

            // 禁用Session(使用无状态认证)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;

return http.build();
    }

@Bean
public IpWhitelistFilter ipWhitelistFilter(){
returnnew IpWhitelistFilter(ipWhitelistService);
    }
}

/**
 * Spring Security IP过滤器
 */

classIpWhitelistFilterextendsOncePerRequestFilter{

privatefinal IpWhitelistService ipWhitelistService;

publicIpWhitelistFilter(IpWhitelistService ipWhitelistService){
this.ipWhitelistService = ipWhitelistService;
    }

@Override
protectedvoiddoFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain)
throws IOException, ServletException 
{

        String clientIp = getClientIp(request);
        String requestUri = request.getRequestURI();

// 跳过不需要检查的路径
if (shouldSkip(requestUri)) {
            filterChain.doFilter(request, response);
return;
        }

// 检查IP白名单
if (!ipWhitelistService.isIpAllowed(clientIp)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json");
            response.getWriter().write(
                String.format("{\"error\":\"Access denied for IP: %s\"}", clientIp)
            );
return;
        }

        filterChain.doFilter(request, response);
    }

private String getClientIp(HttpServletRequest request){
// 同前面的提取逻辑
// ...
    }

privatebooleanshouldSkip(String uri){
// 同前面的跳过逻辑
// ...
    }
}

五、避坑大全:这些坑我帮你踩过了

坑1:反向代理导致的IP获取错误(最常见!)

症状:明明配置了白名单,还是403原因:Nginx/Apache没转发真实IP解决

# Nginx必须配置
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

坑2:IPv6 vs IPv4不匹配

症状:服务器IPv6,客户端IPv4,匹配不上解决:

publicclassIpUtils{

publicstatic String normalizeIp(String ip){
if (ip == null) returnnull;

// 处理IPv6映射的IPv4地址
if (ip.startsWith("::ffff:")) {
return ip.substring(7);
        }

// 处理IPv6简写
if (ip.contains("::")) {
// 将简写IPv6转换为完整格式
return expandIPv6(ip);
        }

return ip;
    }

privatestatic String expandIPv6(String ip){
// IPv6扩展逻辑
// ...
    }
}

坑3:IP段支持(如192.168.1.*)

需求:支持通配符和CIDR表示法实现:

publicclassIpPatternMatcher{

publicstaticbooleanmatches(String pattern, String ip){
if (pattern == null || ip == null) returnfalse;

// 精确匹配
if (!pattern.contains("*") && !pattern.contains("/")) {
return pattern.equals(ip);
        }

// 通配符匹配:192.168.1.*
if (pattern.contains("*")) {
            String regex = pattern
                .replace(".", "\\.")
                .replace("*", "\\d+");
return ip.matches(regex);
        }

// CIDR匹配:192.168.1.0/24
if (pattern.contains("/")) {
return matchCidr(pattern, ip);
        }

returnfalse;
    }

privatestaticbooleanmatchCidr(String cidr, String ip){
try {
            SubnetUtils utils = new SubnetUtils(cidr);
return utils.getInfo().isInRange(ip);
        } catch (Exception e) {
returnfalse;
        }
    }
}

坑4:缓存雪崩

场景:缓存同时失效,大量请求打向数据库

解决:差异化过期时间

// 在缓存TTL上加随机偏移
privatelonggetRandomTtl(){
long baseTtl = 30 * 60; // 30分钟
long randomOffset = ThreadLocalRandom.current().nextLong(-5 * 60, 5 * 60); // ±5分钟
return baseTtl + randomOffset;
}

坑5:管理接口没保护

风险:白名单管理接口被黑客访问

解决:管理接口必须加额外保护

@RestController
@RequestMapping("/api/system/ip-whitelist")
@PreAuthorize("hasRole('ADMIN')") // 必须管理员角色
publicclassIpWhitelistAdminController{
// ...
}

六、监控与告警:知道谁在敲门

白名单不是设完就完了,要监控异常行为:

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Component
publicclassIpAccessMonitor{

// 记录非法IP尝试次数
privatefinal Map<String, AtomicInteger> illegalAttempts = new ConcurrentHashMap<>();

// 记录合法IP访问频率
privatefinal Map<String, AccessRecord> legalAccess = new ConcurrentHashMap<>();

/**
     * 记录非法访问
     */

publicvoidrecordIllegalAttempt(String ip, String uri){
        AtomicInteger counter = illegalAttempts.computeIfAbsent(
            ip, k -> new AtomicInteger(0)
        );

int attempts = counter.incrementAndGet();
        log.warn("非法IP访问:{} -> {},累计次数:{}", ip, uri, attempts);

// 同一个IP尝试超过阈值,触发告警
if (attempts >= 10) {
            sendAlert(ip, attempts, uri);
        }
    }

/**
     * 记录合法访问
     */

publicvoidrecordLegalAccess(String ip, String uri){
        AccessRecord record = legalAccess.computeIfAbsent(
            ip, k -> new AccessRecord()
        );

        record.recordAccess();

// 检查是否访问过于频繁
if (record.isTooFrequent()) {
            log.warn("IP访问过于频繁:{} -> {},频率:{}/分钟", 
                ip, uri, record.getAccessPerMinute());
        }
    }

/**
     * 发送告警
     */

privatevoidsendAlert(String ip, int attempts, String uri){
// 发送邮件、钉钉、企业微信告警
        String message = String.format(
"安全告警\n" +
"IP:%s\n" +
"异常行为:非法访问尝试\n" +
"累计次数:%d\n" +
"目标接口:%s\n" +
"时间:%s",
            ip, attempts, uri, new java.util.Date()
        );

// 实际发送告警逻辑
        log.error("告警:{}", message);
    }

/**
     * 定时清理记录
     */

@Scheduled(cron = "0 0 * * * ?") // 每小时执行
publicvoidclearOldRecords(){
// 清理1小时前的记录
long oneHourAgo = System.currentTimeMillis() - 3600000;

        illegalAttempts.entrySet().removeIf(entry -> 
            entry.getValue().get() < 5// 保留尝试5次以上的记录
        );

        legalAccess.entrySet().removeIf(entry ->
            entry.getValue().getLastAccessTime() < oneHourAgo
        );
    }
}

/**
 * 访问记录
 */

classAccessRecord{
privatefinal List<Long> accessTimestamps = new CopyOnWriteArrayList<>();

publicvoidrecordAccess(){
long now = System.currentTimeMillis();
        accessTimestamps.add(now);

// 只保留最近1分钟的记录
long oneMinuteAgo = now - 60000;
        accessTimestamps.removeIf(timestamp -> timestamp < oneMinuteAgo);
    }

publicbooleanisTooFrequent(){
return getAccessPerMinute() > 100; // 阈值:100次/分钟
    }

publicdoublegetAccessPerMinute(){
long oneMinuteAgo = System.currentTimeMillis() - 60000;
long count = accessTimestamps.stream()
            .filter(timestamp -> timestamp > oneMinuteAgo)
            .count();
return count;
    }

publiclonggetLastAccessTime(){
if (accessTimestamps.isEmpty()) {
return0;
        }
return accessTimestamps.get(accessTimestamps.size() - 1);
    }
}

七、总结:安全是个系统工程

方案
适用场景
优点
缺点
静态白名单
固定IP、测试环境
简单快速、零依赖
改IP需重启
动态白名单
生产环境、IP变动频繁
热更新、可管理
需要数据库+缓存
Spring Security集成
已有权限体系项目
统一管理、粒度细
学习成本高

最佳实践清单:

  1. 分层防护:IP白名单 + 接口限流 + 身份认证 + 参数校验
  2. 最小权限:只开放必要IP,能内网不公网
  3. 定期审计:每月review白名单,清理无效IP
  4. 监控告警:实时监控异常访问,及时响应
  5. 备份方案:保留紧急访问通道(如VPN+固定IP)
  6. 文档完善:白名单管理流程文档化

八、说两句题外话

凌晨三点的告警电话,从来不会提前预约。接口安全就像买保险——平时觉得多余,出事时才知道真香。

今天加的这几行IP白名单代码,可能就是你明天安稳睡觉的保证。别等数据库被刷爆了才后悔,现在就动手吧!

记住:在安全的世界里,偏执不是病,而是一种美德。你的系统越"难搞",黑客就越不想搞你。

实战作业:

  1. 在你的SpringBoot项目中选一个敏感接口,用动态白名单方案保护起来
  2. 实现一个IP分析面板,展示最近被拒绝的IP地址和尝试次数
  3. 配置告警规则,当同一IP短时间内频繁访问时自动告警

扩展挑战:

  • 支持IPv6地址
  • 实现IP地理位置显示(可用免费IP库)
  • 集成到公司统一的权限管理系统

祝大家的接口永远不被"薅秃"!

Image