凌晨三点接口被刷,我用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,能内网不公网 定期审计:每月review白名单,清理无效IP 监控告警:实时监控异常访问,及时响应 备份方案:保留紧急访问通道(如VPN+固定IP) 文档完善:白名单管理流程文档化
八、说两句题外话
凌晨三点的告警电话,从来不会提前预约。接口安全就像买保险——平时觉得多余,出事时才知道真香。
今天加的这几行IP白名单代码,可能就是你明天安稳睡觉的保证。别等数据库被刷爆了才后悔,现在就动手吧!
记住:在安全的世界里,偏执不是病,而是一种美德。你的系统越"难搞",黑客就越不想搞你。
实战作业:
在你的SpringBoot项目中选一个敏感接口,用动态白名单方案保护起来 实现一个IP分析面板,展示最近被拒绝的IP地址和尝试次数 配置告警规则,当同一IP短时间内频繁访问时自动告警
扩展挑战:
支持IPv6地址 实现IP地理位置显示(可用免费IP库) 集成到公司统一的权限管理系统
祝大家的接口永远不被"薅秃"!
以为能躺赚,结果“养虾”变成了“养雷”,第一批“养虾人”已经失眠了……
DeepSeek被针对,Anthropic指控三家中国AI蒸馏剽窃,马斯克硬刚“贼喊抓贼”!
明明大厂裁员滚滚,为什么运维还这么难招?
在 SQL 中写了 in 和 not in,技术总监让我明天不用来了
年底了!系统稳如狗,甲方觉得我们没工作量,怎么收运维费?
为什么DeepSeek火之后,人们想到的是大量裁员,而不是实行上三休四?
《AI数据分析之ChatBI发展与应用实践》白皮书(附下载)正式上线啦
号外!《核心系统分布式数据库选型指南》电子书(附下载)正式上线
解锁数据架构现代化密码,《实时数仓选型指南》电子书(附下载)正式上线啦