手把手教你构建API防火墙,从Token到限流,一套代码彻底杜绝接口盗用

0 阅读6分钟

大家好,我是小悟。

一、需求分析

1.1 核心需求

  • 身份认证:确保请求来自合法用户/应用
  • 请求防篡改:保证请求内容在传输过程中未被修改
  • 防重放攻击:防止合法请求被恶意重复发送
  • 频率控制:限制单位时间内的请求次数
  • 来源验证:确保请求来自合法的前端应用

1.2 安全层次

客户端层 → 传输层 → 服务端层
   ↓         ↓         ↓
签名生成   HTTPS加密  签名验证
Token管理  证书校验   频率限制
请求加密              IP白名单

二、详细实现步骤

2.1 技术选型

  • 后端框架:Spring Boot 2.7+
  • 缓存:Redis(存储Token、计数)
  • 加密算法:HMAC-SHA256、AES
  • Token方案:JWT + Redis双重验证

2.2 整体架构图

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   客户端     │      │  网关/拦截器 │      │   业务系统   │
├─────────────┤      ├─────────────┤      ├─────────────┤
│1.获取Token   │─────>│             │      │             │
│2.生成签名    │      │3.验证签名    │─────>│             │
│3.发起请求    │      │4.频率限制    │      │             │
│             │<─────│5.IP白名单    │<─────│             │
└─────────────┘      └─────────────┘      └─────────────┘

三、核心代码实现

3.1 项目依赖(pom.xml)

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 工具类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>31.1-jre</version>
    </dependency>
</dependencies>

3.2 配置文件(application.yml)

server:
  port: 8080

spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 5000ms

api:
  security:
    # Token有效期(分钟)
    token-expire: 30
    # 签名有效期(秒)
    sign-expire: 300
    # 接口限流配置
    rate-limit:
      # 每秒允许请求数
      permits-per-second: 10
      # 限流时长(秒)
      duration: 60
    # IP白名单
    ip-whitelist:
      - 127.0.0.1
      - 192.168.1.0/24

3.3 签名工具类

package com.example.security.util;

import org.apache.commons.codec.digest.HmacUtils;
import org.springframework.stereotype.Component;
import java.util.*;

@Component
public class SignUtil {
    
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String SECRET_KEY = "your-secret-key-here-change-in-production";
    
    /**
     * 生成签名
     * @param params 请求参数
     * @param timestamp 时间戳
     * @param nonce 随机数
     * @return 签名字符串
     */
    public String generateSign(Map<String, Object> params, String timestamp, String nonce) {
        // 1. 参数排序
        TreeMap<String, Object> sortedParams = new TreeMap<>(params);
        
        // 2. 构建待签名字符串
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
            if (entry.getValue() != null && !"sign".equals(entry.getKey())) {
                sb.append(entry.getKey())
                  .append("=")
                  .append(entry.getValue())
                  .append("&");
            }
        }
        sb.append("timestamp=").append(timestamp)
          .append("&nonce=").append(nonce);
        
        // 3. HMAC-SHA256加密
        return HmacUtils.hmacSha256Hex(SECRET_KEY, sb.toString());
    }
    
    /**
     * 验证签名
     */
    public boolean verifySign(Map<String, Object> params, String sign, 
                              String timestamp, String nonce) {
        // 验证时间戳(防止重放攻击)
        long currentTime = System.currentTimeMillis() / 1000;
        long requestTime = Long.parseLong(timestamp);
        if (currentTime - requestTime > 300) { // 5分钟有效期
            return false;
        }
        
        // 验证nonce是否已使用(Redis中存储)
        // 这里需要调用Redis服务,省略具体实现
        
        String expectedSign = generateSign(params, timestamp, nonce);
        return expectedSign.equals(sign);
    }
}

3.4 JWT Token管理

package com.example.security.service;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class TokenService {
    
    @Value("${api.security.token-expire}")
    private Integer tokenExpire;
    
    private final StringRedisTemplate redisTemplate;
    private final SecretKey secretKey;
    
    public TokenService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        String secret = "your-256-bit-secret-key-for-jwt-generation-here";
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }
    
    /**
     * 生成Token
     */
    public String generateToken(String userId) {
        String tokenId = UUID.randomUUID().toString();
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + tokenExpire * 60 * 1000);
        
        String jwt = Jwts.builder()
                .setId(tokenId)
                .setSubject(userId)
                .setIssuedAt(now)
                .setExpiration(expireDate)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();
        
        // 存储到Redis(支持服务端主动失效)
        redisTemplate.opsForValue().set(
            "token:" + tokenId, 
            userId, 
            tokenExpire, 
            TimeUnit.MINUTES
        );
        
        return jwt;
    }
    
    /**
     * 验证Token
     */
    public boolean validateToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            
            String tokenId = claims.getId();
            // 检查Redis中是否存在
            Boolean hasKey = redisTemplate.hasKey("token:" + tokenId);
            return hasKey != null && hasKey;
            
        } catch (ExpiredJwtException e) {
            return false;
        } catch (JwtException e) {
            return false;
        }
    }
    
    /**
     * 注销Token
     */
    public void logout(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            String tokenId = claims.getId();
            redisTemplate.delete("token:" + tokenId);
        } catch (Exception e) {
            // 忽略异常
        }
    }
}

3.5 接口限流器(基于Redis + 令牌桶)

package com.example.security.interceptor;

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Component
public class RateLimiterService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 本地缓存限流器(针对高频访问)
    private final ConcurrentHashMap<String, RateLimiter> localLimiters = new ConcurrentHashMap<>();
    
    private static final int DEFAULT_PERMITS = 10;
    private static final int DEFAULT_DURATION = 60;
    
    /**
     * 检查是否允许访问(基于IP + 接口)
     */
    public boolean allowRequest(String key, int permitsPerSecond, int duration) {
        // 方式1:使用Guava RateLimiter(本地限流)
        RateLimiter limiter = localLimiters.computeIfAbsent(
            key, 
            k -> RateLimiter.create(permitsPerSecond)
        );
        
        if (!limiter.tryAcquire()) {
            return false;
        }
        
        // 方式2:使用Redis实现分布式限流(滑动窗口)
        String redisKey = "rate_limit:" + key;
        Long currentTime = System.currentTimeMillis() / 1000;
        
        // 使用Redis的ZSet实现滑动窗口
        redisTemplate.opsForZSet().add(redisKey, String.valueOf(currentTime), currentTime);
        redisTemplate.expire(redisKey, duration, TimeUnit.SECONDS);
        
        // 统计当前时间窗口内的请求数
        Long count = redisTemplate.opsForZSet().count(
            redisKey, 
            currentTime - duration, 
            currentTime
        );
        
        return count != null && count <= permitsPerSecond * duration;
    }
    
    /**
     * 更精确的令牌桶算法实现(Redis Lua脚本)
     */
    public boolean allowRequestWithLua(String key, int permitsPerSecond) {
        String luaScript = 
            "local key = KEYS[1] " +
            "local permits = tonumber(ARGV[1]) " +
            "local current = redis.call('get', key) " +
            "if current and tonumber(current) > 0 then " +
            "    redis.call('decr', key) " +
            "    return 1 " +
            "else " +
            "    return 0 " +
            "end";
        
        // 初始化令牌桶
        redisTemplate.opsForValue().setIfAbsent(key, String.valueOf(permitsPerSecond));
        redisTemplate.expire(key, 1, TimeUnit.SECONDS);
        
        // 执行Lua脚本
        Long result = redisTemplate.execute(
            (connection) -> connection.eval(
                luaScript.getBytes(),
                ReturnType.INTEGER,
                1,
                key.getBytes(),
                String.valueOf(1).getBytes()
            ),
            (key)
        );
        
        return result != null && result == 1;
    }
}

3.6 核心拦截器

package com.example.security.interceptor;

import com.example.security.util.SignUtil;
import com.example.security.service.TokenService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

@Component
public class ApiSecurityInterceptor implements HandlerInterceptor {
    
    @Autowired
    private TokenService tokenService;
    
    @Autowired
    private SignUtil signUtil;
    
    @Autowired
    private RateLimiterService rateLimiterService;
    
    @Value("${api.security.rate-limit.permits-per-second}")
    private int permitsPerSecond;
    
    @Value("${api.security.rate-limit.duration}")
    private int duration;
    
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        // 1. IP白名单检查
        String clientIp = getClientIp(request);
        if (!checkIpWhitelist(clientIp)) {
            sendError(response, 403, "IP not allowed");
            return false;
        }
        
        // 2. 获取请求头
        String token = request.getHeader("X-Auth-Token");
        String sign = request.getHeader("X-Sign");
        String timestamp = request.getHeader("X-Timestamp");
        String nonce = request.getHeader("X-Nonce");
        
        // 3. Token验证
        if (token == null || !tokenService.validateToken(token)) {
            sendError(response, 401, "Invalid or expired token");
            return false;
        }
        
        // 4. 验证必要参数
        if (sign == null || timestamp == null || nonce == null) {
            sendError(response, 400, "Missing security headers");
            return false;
        }
        
        // 5. 获取请求参数
        Map<String, Object> params = getRequestParams(request);
        
        // 6. 签名验证
        if (!signUtil.verifySign(params, sign, timestamp, nonce)) {
            sendError(response, 401, "Invalid signature");
            return false;
        }
        
        // 7. 频率限制(基于用户+接口)
        String rateLimitKey = token + ":" + request.getRequestURI();
        if (!rateLimiterService.allowRequest(rateLimitKey, permitsPerSecond, duration)) {
            sendError(response, 429, "Too many requests");
            return false;
        }
        
        return true;
    }
    
    /**
     * 获取客户端真实IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 多级代理取第一个IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
    
    /**
     * IP白名单检查(支持CIDR格式)
     */
    private boolean checkIpWhitelist(String clientIp) {
        // 从配置读取白名单列表
        List<String> whitelist = Arrays.asList("127.0.0.1", "192.168.1.0/24");
        
        for (String allowed : whitelist) {
            if (allowed.contains("/")) {
                // CIDR格式匹配
                if (isInCidr(clientIp, allowed)) {
                    return true;
                }
            } else {
                if (allowed.equals(clientIp)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * CIDR匹配
     */
    private boolean isInCidr(String ip, String cidr) {
        // 简化实现,实际可使用Apache Commons Net或自行实现
        // 这里省略具体实现
        return true;
    }
    
    /**
     * 获取请求参数(GET和POST)
     */
    @SuppressWarnings("unchecked")
    private Map<String, Object> getRequestParams(HttpServletRequest request) throws Exception {
        Map<String, Object> params = new HashMap<>();
        
        // GET参数
        Map<String, String[]> paramMap = request.getParameterMap();
        for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
            params.put(entry.getKey(), entry.getValue()[0]);
        }
        
        // POST JSON参数
        if ("POST".equalsIgnoreCase(request.getMethod()) && 
            request.getContentType() != null && 
            request.getContentType().contains("application/json")) {
            Map<String, Object> jsonParams = objectMapper.readValue(
                request.getInputStream(), 
                Map.class
            );
            params.putAll(jsonParams);
        }
        
        return params;
    }
    
    private void sendError(HttpServletResponse response, int status, String message) 
            throws Exception {
        response.setStatus(status);
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> result = new HashMap<>();
        result.put("code", status);
        result.put("message", message);
        result.put("timestamp", System.currentTimeMillis());
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

3.7 配置拦截器

package com.example.security.config;

import com.example.security.interceptor.ApiSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private ApiSecurityInterceptor apiSecurityInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiSecurityInterceptor)
                .addPathPatterns("/api/**")  // 拦截所有API接口
                .excludePathPatterns(
                    "/api/auth/**",          // 认证接口放行
                    "/api/public/**",        // 公共接口放行
                    "/api/health"            // 健康检查接口
                );
    }
}

3.8 客户端实现示例(JavaScript)

// 前端签名工具类
class ApiSecurity {
    constructor(appKey, appSecret) {
        this.appKey = appKey;
        this.appSecret = appSecret;
    }
    
    // 生成随机数
    generateNonce() {
        return Math.random().toString(36).substring(2, 15);
    }
    
    // 生成时间戳
    getTimestamp() {
        return Math.floor(Date.now() / 1000);
    }
    
    // 参数排序并拼接
    sortParams(params) {
        const keys = Object.keys(params).sort();
        let str = '';
        for (let key of keys) {
            if (key !== 'sign' && params[key] !== undefined && params[key] !== null) {
                str += `${key}=${params[key]}&`;
            }
        }
        return str;
    }
    
    // 生成签名
    async generateSign(params) {
        const timestamp = this.getTimestamp();
        const nonce = this.generateNonce();
        
        const sortedStr = this.sortParams(params);
        const signStr = `${sortedStr}timestamp=${timestamp}&nonce=${nonce}`;
        
        // 使用HMAC-SHA256加密
        const encoder = new TextEncoder();
        const keyData = encoder.encode(this.appSecret);
        const messageData = encoder.encode(signStr);
        
        const cryptoKey = await crypto.subtle.importKey(
            'raw',
            keyData,
            { name: 'HMAC', hash: 'SHA-256' },
            false,
            ['sign']
        );
        
        const signature = await crypto.subtle.sign(
            'HMAC',
            cryptoKey,
            messageData
        );
        
        // 转换为十六进制
        const hashArray = Array.from(new Uint8Array(signature));
        const signHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
        
        return {
            sign: signHex,
            timestamp,
            nonce
        };
    }
    
    // 发起请求
    async request(url, method, data = null) {
        const token = localStorage.getItem('auth_token');
        const { sign, timestamp, nonce } = await this.generateSign(data || {});
        
        const headers = {
            'Content-Type': 'application/json',
            'X-Auth-Token': token,
            'X-Sign': sign,
            'X-Timestamp': timestamp,
            'X-Nonce': nonce
        };
        
        const options = {
            method: method,
            headers: headers
        };
        
        if (data && method !== 'GET') {
            options.body = JSON.stringify(data);
        }
        
        const response = await fetch(url, options);
        
        if (response.status === 401) {
            // Token失效,跳转登录
            window.location.href = '/login';
        }
        
        if (response.status === 429) {
            console.error('请求过于频繁,请稍后再试');
        }
        
        return response.json();
    }
}

// 使用示例
const api = new ApiSecurity('your_app_key', 'your_app_secret');
api.request('/api/user/info', 'GET').then(data => {
    console.log(data);
});

四、详细总结

4.1 实现的核心安全机制

  1. 多层防护体系
    • 传输层:强制HTTPS加密,防止中间人攻击
    • 认证层:JWT + Redis双重验证,支持服务端主动失效
    • 签名层:HMAC-SHA256签名,防止参数篡改
    • 限流层:令牌桶算法 + 滑动窗口,防止CC攻击
    • 访问层:IP白名单,限制访问来源
  2. 防重放攻击策略
    • 时间戳验证:5分钟有效期
    • Nonce随机数:Redis缓存已使用的nonce
    • Token轮换:定期刷新token
  3. 性能优化
    • Guava本地限流器 + Redis分布式限流结合
    • 签名计算缓存
    • 异步日志记录

4.2 最佳实践

  1. 密钥管理

    // 使用配置中心管理密钥,定期轮换
    @ConfigurationProperties(prefix = "api.secret")
    public class SecretConfig {
        private String key;
        private String version;
        // 支持密钥版本管理
    }
    
  2. 异常处理

    • 统一返回格式,不暴露系统内部信息
    • 记录所有安全异常到独立日志
    • 敏感操作触发告警
  3. 监控告警

    @Component
    public class SecurityMonitor {
        // 记录异常请求
        public void recordAbnormalRequest(String ip, String reason) {
            // 使用ELK或Prometheus监控
            // 短时间内异常次数过多自动加入黑名单
        }
    }
    

4.3 安全性评估

攻击类型防护措施有效性
重放攻击时间戳+Nonce★★★★★
参数篡改HMAC签名★★★★★
CC攻击多级限流★★★★☆
Token窃取HTTPS+短时效★★★★☆
越权访问Token绑定用户★★★★★
中间人攻击HTTPS强制★★★★★

4.4 运维注意事项

  1. Redis高可用

    • 使用Redis Cluster或Sentinel
    • 设置合理的过期策略
    • 监控内存使用情况
  2. 性能指标

    • 签名验证耗时:< 5ms
    • 限流检查耗时:< 2ms
    • Token验证耗时:< 3ms
  3. 配置调优

    yaml

    api:
      security:
        # 根据业务调整
        rate-limit:
          # 登录接口:5次/分钟
          login: 5/60
          # 查询接口:100次/分钟  
          query: 100/60
          # 写入接口:20次/分钟
          write: 20/60
    

4.5 扩展建议

  1. 加入验证码机制
    • 登录失败3次后要求验证码
    • 使用Google reCAPTCHA或自研验证码
  2. 设备指纹
    • 记录设备指纹信息
    • 异常设备切换触发二次验证
  3. 行为分析
    • 机器学习识别异常行为模式
    • 自动化IP封禁系统
  4. API网关
    • 考虑使用Spring Cloud Gateway
    • 统一管理所有安全策略
    • 支持动态路由和限流

4.6 常见问题及解决方案

Q: 签名验证失败率高? A: 检查客户端和服务端时间同步,使用NTP服务;确保参数排序算法一致

Q: 限流误伤正常用户? A: 针对不同接口设置差异化限流策略;使用滑动窗口算法更平滑

Q: Token如何刷新? A: 使用双Token机制:Access Token(短时效)+ Refresh Token(长时效)

Q: 分布式环境下的限流精度? A: 使用Redis + Lua脚本保证原子性;考虑使用Sentinel等专业组件

通过以上方案的实施,可以有效防止API接口被恶意调用,保障系统的安全性和稳定性。根据实际业务场景,在安全性和用户体验之间找到平衡点,持续优化和完善安全策略。

手把手教你构建API防火墙,从Token到限流,一套代码彻底杜绝接口盗用.png

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海