最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot實(shí)現(xiàn)短信驗(yàn)證碼接口防刷的完整方案

 更新時(shí)間:2026年03月16日 08:38:44   作者:J_liaty  
本文介紹了在高并發(fā)場景下構(gòu)建分層防護(hù)體系,以保障短信驗(yàn)證碼接口的安全性、性能和成本控制,通過分層防護(hù)、多維度限流、異步化處理、監(jiān)控告警等策略,有效應(yīng)對(duì)各種攻擊類型,提升系統(tǒng)的整體安全性和可用性,需要的朋友可以參考下

核心目標(biāo):在高并發(fā)場景下,構(gòu)建分層防護(hù)體系,既保障接口安全,又保證響應(yīng)速度,同時(shí)控制短信成本。

一、問題背景與挑戰(zhàn)

1.1 為什么需要防刷?

短信驗(yàn)證碼接口是攻擊者的高價(jià)值目標(biāo),主要原因:

  • 直接利益驅(qū)動(dòng):刷 單、薅羊毛、惡意注冊(cè)
  • 成本敏感:短信成本按條計(jì)費(fèi),刷量會(huì)導(dǎo)致成本激增
  • 業(yè)務(wù)影響:真實(shí)用戶體驗(yàn)下降,品牌受損

1.2 典型攻擊場景

攻擊類型攻擊手段防護(hù)難點(diǎn)
暴力刷量單IP高頻請(qǐng)求不同手機(jī)號(hào)識(shí)別IP偽裝
代理IP池使用代理IP切換發(fā)送設(shè)備指紋識(shí)別
打碼平臺(tái)人工+自動(dòng)化腳本繞過圖形驗(yàn)證行為分析
重放攻擊攔截請(qǐng)求重復(fù)發(fā)送簽名校驗(yàn)
分布式攻擊多節(jié)點(diǎn)協(xié)同攻擊全局限流

1.3 防護(hù)目標(biāo)

  • 安全性:攔截90%以上的惡意請(qǐng)求
  • 性能:P99響應(yīng)時(shí)間 < 100ms
  • 可用性:99.99%可用性,保證真實(shí)用戶不受影響
  • 成本:短信成本控制在合理范圍

二、整體架構(gòu)設(shè)計(jì)

2.1 分層防護(hù)體系

┌─────────────────────────────────────────────────────────────┐
│                      CDN/WAF層                               │
│  - DDoS防護(hù)  - IP黑名單  - 地域封禁                          │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────────┐
│                    網(wǎng)關(guān)層(Nginx/API Gateway)               │
│  - 全局限流  - IP封禁  - 協(xié)議過濾                           │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────────┐
│                  應(yīng)用層(后端服務(wù))                          │
│  - 簽名校驗(yàn)  - 圖形驗(yàn)證  - 業(yè)務(wù)限流                          │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────────┐
│                   緩存層(Redis Cluster)                     │
│  - 計(jì)數(shù)器存儲(chǔ)  - 黑名單緩存  - 分布式鎖                       │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────────┐
│                  數(shù)據(jù)層(MySQL/消息隊(duì)列)                     │
│  - 記錄日志  - 異步發(fā)送短信  - 持久化數(shù)據(jù)                     │
└─────────────────────────────────────────────────────────────┘

2.2 防護(hù)層級(jí)說明

防護(hù)層核心能力技術(shù)手段響應(yīng)時(shí)間
CDN/WAF攔截已知攻擊IP信譽(yù)庫、攻擊特征匹配< 10ms
網(wǎng)關(guān)層全局流量控制限流算法、IP封禁< 5ms
應(yīng)用層業(yè)務(wù)邏輯驗(yàn)證簽名、驗(yàn)證碼、限流10-50ms
緩存層高速狀態(tài)存儲(chǔ)Redis計(jì)數(shù)器、分布式鎖< 10ms
數(shù)據(jù)層數(shù)據(jù)持久化異步寫入、消息隊(duì)列不阻塞

三、核心防護(hù)策略詳解

3.1 前端防護(hù)層

3.1.1 按鈕倒計(jì)時(shí)

// 前端實(shí)現(xiàn)示例
let countdown = 60;
let timer = null;
function sendSmsCode(phone) {
    // 倒計(jì)時(shí)中禁用
    if (countdown < 60) {
        showToast('請(qǐng)等待倒計(jì)時(shí)結(jié)束');
        return;
    }
    // 發(fā)送請(qǐng)求
    $.ajax({
        url: '/api/sms/send',
        method: 'POST',
        data: { phone: phone },
        success: function(res) {
            if (res.code === 0) {
                startCountdown();
            }
        }
    });
}
function startCountdown() {
    const btn = $('#send-sms-btn');
    btn.prop('disabled', true);
    timer = setInterval(() => {
        countdown--;
        btn.text(`${countdown}秒后重試`);
        if (countdown <= 0) {
            clearInterval(timer);
            btn.prop('disabled', false);
            btn.text('發(fā)送驗(yàn)證碼');
            countdown = 60;
        }
    }, 1000);
}

注意事項(xiàng)

  • 倒計(jì)時(shí)結(jié)束后必須重新獲取驗(yàn)證碼token
  • 防止用戶修改頁面代碼繞過
  • 前端限制不能替代后端驗(yàn)證

3.1.2 圖形驗(yàn)證碼

實(shí)現(xiàn)邏輯

  1. 用戶點(diǎn)擊"發(fā)送驗(yàn)證碼"
  2. 彈出圖形驗(yàn)證碼(滑塊/點(diǎn)選/旋轉(zhuǎn))
  3. 驗(yàn)證通過后獲得臨時(shí)token
  4. 攜帶token請(qǐng)求短信接口
// 圖形驗(yàn)證碼生成(使用Google Guava)
public class CaptchaService {
    
    /**
     * 生成滑塊驗(yàn)證碼
     */
    public CaptchaVO generateSliderCaptcha(String sessionId) {
        // 1. 生成滑塊和背景圖
        SliderImageResult result = sliderCaptchaGenerator.generate();
        
        // 2. 存儲(chǔ)驗(yàn)證信息到Redis,5分鐘過期
        String captchaKey = RedisKeyConstant.SLIDER_CAPTCHA + sessionId;
        CaptchaInfo captchaInfo = new CaptchaInfo(
            result.getX(),
            result.getY(),
            System.currentTimeMillis()
        );
        
        // 使用Hash結(jié)構(gòu)存儲(chǔ),支持部分字段查詢
        redisTemplate.opsForHash().putAll(captchaKey, captchaInfo.toMap());
        redisTemplate.expire(captchaKey, 5, TimeUnit.MINUTES);
        
        // 3. 返回前端信息(不含真實(shí)坐標(biāo))
        return CaptchaVO.builder()
            .sessionId(sessionId)
            .backgroundImage(result.getBackgroundImage())
            .sliderImage(result.getSliderImage())
            .token(UUID.randomUUID().toString())  // 臨時(shí)token
            .build();
    }
    
    /**
     * 驗(yàn)證滑塊位置
     */
    public boolean verifySliderCaptcha(String sessionId, String token, 
                                       int userX, int userY) {
        String captchaKey = RedisKeyConstant.SLIDER_CAPTCHA + sessionId;
        
        // 獲取存儲(chǔ)的驗(yàn)證信息
        Map<Object, Object> captchaData = redisTemplate.opsForHash()
            .entries(captchaKey);
        
        if (captchaData.isEmpty()) {
            return false;
        }
        
        int realX = (int) captchaData.get("x");
        int realY = (int) captchaData.get("y");
        
        // 允許誤差范圍:X軸±5像素,Y軸±10像素
        boolean isValid = Math.abs(userX - realX) <= 5 && 
                          Math.abs(userY - realY) <= 10;
        
        // 驗(yàn)證通過后刪除驗(yàn)證碼
        if (isValid) {
            redisTemplate.delete(captchaKey);
        }
        
        return isValid;
    }
}

高級(jí)防護(hù)

  • 軌跡分析:記錄鼠標(biāo)移動(dòng)軌跡,判斷是否真人操作
  • 時(shí)間檢測:驗(yàn)證響應(yīng)時(shí)間 < 1秒判定為腳本
  • 設(shè)備指紋:生成設(shè)備唯一標(biāo)識(shí),防止繞過

3.2 網(wǎng)關(guān)層防護(hù)

3.2.1 Nginx限流配置

# 限流配置:定義限流區(qū)域
limit_req_zone $binary_remote_addr zone=sms_limit:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=sms_conn:10m;
server {
    listen 80;
    location /api/sms/ {
        # 請(qǐng)求限流:每秒5個(gè)請(qǐng)求,允許突發(fā)10個(gè)
        limit_req zone=sms_limit burst=10 nodelay;
        # 連接數(shù)限制:同一IP最多20個(gè)并發(fā)連接
        limit_conn sms_conn 20;
        # 限流返回狀態(tài)碼
        limit_req_status 429;
        # 限流后返回JSON響應(yīng)
        error_page 429 = @rate_limited;
        proxy_pass http://backend;
    }
    location @rate_limited {
        default_type application/json;
        return 429 '{"code": 429, "msg": "請(qǐng)求過于頻繁,請(qǐng)稍后再試"}';
    }
}

參數(shù)說明

  • zone=sms_limit:10m:限流區(qū)域名稱,10MB內(nèi)存(可存約16萬個(gè)IP狀態(tài))
  • rate=5r/s:每秒允許5個(gè)請(qǐng)求
  • burst=10:允許突發(fā)10個(gè)請(qǐng)求(緩沖區(qū)大小)
  • nodelay:超過burst立即拒絕,不延遲處理

3.2.2 IP黑名單動(dòng)態(tài)管理

/**
 * IP黑名單管理服務(wù)
 */
@Service
public class IpBlacklistService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String BLACKLIST_KEY = "security:ip:blacklist";
    private static final long EXPIRE_HOURS = 24;
    
    /**
     * 檢查IP是否在黑名單中
     */
    public boolean isBlacklisted(String ip) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForSet().isMember(BLACKLIST_KEY, ip)
        );
    }
    
    /**
     * 添加IP到黑名單
     */
    public void addToBlacklist(String ip, String reason) {
        redisTemplate.opsForSet().add(BLACKLIST_KEY, ip);
        
        // 記錄封禁原因和封禁時(shí)間
        String infoKey = BLACKLIST_KEY + ":info:" + ip;
        Map<String, String> info = new HashMap<>();
        info.put("reason", reason);
        info.put("bannedAt", String.valueOf(System.currentTimeMillis()));
        
        redisTemplate.opsForHash().putAll(infoKey, info);
        redisTemplate.expire(infoKey, EXPIRE_HOURS, TimeUnit.HOURS);
        
        // 同時(shí)更新主黑名單的過期時(shí)間
        redisTemplate.expire(BLACKLIST_KEY, EXPIRE_HOURS, TimeUnit.HOURS);
    }
    
    /**
     * 從黑名單移除IP
     */
    public void removeFromBlacklist(String ip) {
        redisTemplate.opsForSet().remove(BLACKLIST_KEY, ip);
        
        String infoKey = BLACKLIST_KEY + ":info:" + ip;
        redisTemplate.delete(infoKey);
    }
    
    /**
     * 獲取黑名單信息
     */
    public BlacklistInfo getBlacklistInfo(String ip) {
        String infoKey = BLACKLIST_KEY + ":info:" + ip;
        Map<Object, Object> data = redisTemplate.opsForHash()
            .entries(infoKey);
        
        if (data.isEmpty()) {
            return null;
        }
        
        return BlacklistInfo.builder()
            .ip(ip)
            .reason((String) data.get("reason"))
            .bannedAt(Long.parseLong((String) data.get("bannedAt")))
            .build();
    }
}

3.3 應(yīng)用層防護(hù)

3.3.1 簽名校驗(yàn)機(jī)制

簽名流程

  1. 前端按字典序排序所有參數(shù)
  2. 拼接密鑰和時(shí)間戳
  3. 計(jì)算MD5/SHA256簽名
  4. 將簽名放在請(qǐng)求頭中
/**
 * 簽名驗(yàn)證工具類
 */
@Component
public class SignatureValidator {
    
    // 簽名密鑰(實(shí)際應(yīng)放在配置中心)
    @Value("${sms.sign.secret}")
    private String signSecret;
    
    // 簽名有效期(5分鐘)
    private static final long SIGN_EXPIRE_TIME = 5 * 60 * 1000;
    
    /**
     * 生成簽名(供前端參考)
     */
    public static String generateSignature(Map<String, String> params, 
                                           String secret, long timestamp) {
        // 1. 按字典序排序參數(shù)
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        
        // 2. 拼接字符串
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
                sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
        }
        
        // 3. 添加密鑰和時(shí)間戳
        sb.append("timestamp=").append(timestamp).append("&");
        sb.append("secret=").append(secret);
        
        // 4. 計(jì)算MD5
        return DigestUtils.md5Hex(sb.toString());
    }
    
    /**
     * 驗(yàn)證簽名
     */
    public boolean validateSignature(HttpServletRequest request, 
                                    Map<String, String> params) {
        // 1. 獲取請(qǐng)求頭中的簽名和時(shí)間戳
        String signature = request.getHeader("X-Signature");
        String timestampStr = request.getHeader("X-Timestamp");
        
        if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestampStr)) {
            return false;
        }
        
        try {
            long timestamp = Long.parseLong(timestampStr);
            
            // 2. 檢查時(shí)間戳是否過期
            if (System.currentTimeMillis() - timestamp > SIGN_EXPIRE_TIME) {
                return false;
            }
            
            // 3. 計(jì)算簽名
            String expectedSignature = generateSignature(params, signSecret, timestamp);
            
            // 4. 比對(duì)簽名(使用防時(shí)序攻擊的比較方法)
            return MessageDigest.isEqual(
                signature.getBytes(StandardCharsets.UTF_8),
                expectedSignature.getBytes(StandardCharsets.UTF_8)
            );
            
        } catch (NumberFormatException e) {
            return false;
        }
    }
}

攔截器集成

/**
 * 簽名驗(yàn)證攔截器
 */
@Component
public class SignatureInterceptor implements HandlerInterceptor {
    
    @Autowired
    private SignatureValidator signatureValidator;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) {
        // 只對(duì)短信接口進(jìn)行簽名驗(yàn)證
        if (!request.getRequestURI().startsWith("/api/sms/")) {
            return true;
        }
        
        // 獲取所有請(qǐng)求參數(shù)
        Map<String, String> params = new HashMap<>();
        request.getParameterMap().forEach((key, values) -> {
            params.put(key, values[0]);
        });
        
        // 驗(yàn)證簽名
        boolean isValid = signatureValidator.validateSignature(request, params);
        
        if (!isValid) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            try {
                response.getWriter().write(
                    "{\"code\": 401, \"msg\": \"簽名驗(yàn)證失敗\"}"
                );
            } catch (IOException e) {
                log.error("寫入響應(yīng)失敗", e);
            }
            return false;
        }
        
        return true;
    }
}

3.3.2 Redis分布式限流器

令牌桶算法實(shí)現(xiàn)

/**
 * 分布式限流器(基于令牌桶算法)
 */
@Component
public class DistributedRateLimiter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * Lua腳本:原子性執(zhí)行限流檢查和扣減
     */
    private static final String RATE_LIMIT_SCRIPT =
        "local key = KEYS[1] " +
        "local capacity = tonumber(ARGV[1]) " +
        "local tokens = tonumber(ARGV[2]) " +
        "local interval = tonumber(ARGV[3]) " +
        
        "local current = redis.call('HMGET', key, 'tokens', 'last_refill') " +
        "local currentTokens = tonumber(current[1]) or capacity " +
        "local lastRefill = tonumber(current[2]) or 0 " +
        
        "local now = tonumber(ARGV[4]) " +
        "local elapsed = now - lastRefill " +
        
        " -- 計(jì)算補(bǔ)充的令牌數(shù) " +
        "if elapsed > 0 then " +
        "    local newTokens = math.min(capacity, currentTokens + elapsed * tokens / interval) " +
        "    currentTokens = newTokens " +
        "end " +
        
        " -- 判斷是否有足夠令牌 " +
        "if currentTokens >= 1 then " +
        "    redis.call('HMSET', key, 'tokens', currentTokens - 1, 'last_refill', now) " +
        "    redis.call('EXPIRE', key, interval / 1000 + 60) " +
        "    return 1 " +  -- 允許通過
        "else " +
        "    redis.call('HMSET', key, 'tokens', currentTokens, 'last_refill', now) " +
        "    redis.call('EXPIRE', key, interval / 1000 + 60) " +
        "    return 0 " +  -- 拒絕請(qǐng)求
        "end";
    
    /**
     * 嘗試獲取令牌
     * 
     * @param key 限流鍵(如:sms:limit:ip:xxx)
     * @param capacity 桶容量
     * @param tokensPerInterval 每個(gè)時(shí)間間隔生成的令牌數(shù)
     * @param interval 時(shí)間間隔(毫秒)
     * @return 是否獲取成功
     */
    public boolean tryAcquire(String key, long capacity, 
                             long tokensPerInterval, long interval) {
        long now = System.currentTimeMillis();
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(RATE_LIMIT_SCRIPT);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList(key),
            String.valueOf(capacity),
            String.valueOf(tokensPerInterval),
            String.valueOf(interval),
            String.valueOf(now)
        );
        
        return result != null && result == 1L;
    }
    
    /**
     * 獲取當(dāng)前剩余令牌數(shù)
     */
    public double getAvailableTokens(String key) {
        Map<Object, Object> data = redisTemplate.opsForHash().entries(key);
        
        if (data.isEmpty()) {
            return 0;
        }
        
        String tokens = (String) data.get("tokens");
        return StringUtils.isEmpty(tokens) ? 0 : Double.parseDouble(tokens);
    }
}

滑動(dòng)窗口限流實(shí)現(xiàn)

/**
 * 滑動(dòng)窗口限流器
 */
@Component
public class SlidingWindowRateLimiter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * Lua腳本:滑動(dòng)窗口計(jì)數(shù)
     */
    private static final String SLIDING_WINDOW_SCRIPT =
        "local key = KEYS[1] " +
        "local now = tonumber(ARGV[1]) " +
        "local windowSize = tonumber(ARGV[2]) " +
        "local maxCount = tonumber(ARGV[3]) " +
        
        " -- 刪除窗口外的數(shù)據(jù) " +
        "redis.call('ZREMRANGEBYSCORE', key, '-inf', now - windowSize) " +
        
        " -- 獲取當(dāng)前窗口內(nèi)的請(qǐng)求數(shù) " +
        "local currentCount = redis.call('ZCARD', key) " +
        
        " -- 判斷是否超限 " +
        "if currentCount < maxCount then " +
        "    redis.call('ZADD', key, now, now) " +
        "    redis.call('EXPIRE', key, windowSize / 1000 + 60) " +
        "    return 1 " +
        "else " +
        "    return 0 " +
        "end";
    
    /**
     * 嘗試通過限流
     * 
     * @param key 限流鍵
     * @param windowSize 窗口大?。ê撩耄?
     * @param maxCount 最大請(qǐng)求數(shù)
     * @return 是否通過
     */
    public boolean tryAcquire(String key, long windowSize, long maxCount) {
        long now = System.currentTimeMillis();
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(SLIDING_WINDOW_SCRIPT);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList(key),
            String.valueOf(now),
            String.valueOf(windowSize),
            String.valueOf(maxCount)
        );
        
        return result != null && result == 1L;
    }
}

3.3.3 多維度限流配置

/**
 * 短信接口限流配置
 */
@Configuration
public class SmsRateLimitConfig {
    
    /**
     * 限流維度配置
     */
    public enum LimitDimension {
        // 單IP每分鐘最多20次
        IP_PER_MINUTE("sms:limit:ip:%s", 60 * 1000, 20),
        
        // 單IP每小時(shí)最多100次
        IP_PER_HOUR("sms:limit:ip:%s", 60 * 60 * 1000, 100),
        
        // 單手機(jī)號(hào)每分鐘最多3次
        PHONE_PER_MINUTE("sms:limit:phone:%s", 60 * 1000, 3),
        
        // 單手機(jī)號(hào)每小時(shí)最多10次
        PHONE_PER_HOUR("sms:limit:phone:%s", 60 * 60 * 1000, 10),
        
        // 單手機(jī)號(hào)每天最多20次
        PHONE_PER_DAY("sms:limit:phone:%s", 24 * 60 * 60 * 1000, 20);
        
        private final String keyPattern;
        private final long windowSize;
        private final long maxCount;
        
        LimitDimension(String keyPattern, long windowSize, long maxCount) {
            this.keyPattern = keyPattern;
            this.windowSize = windowSize;
            this.maxCount = maxCount;
        }
        
        public String buildKey(String identifier) {
            return String.format(keyPattern, identifier);
        }
        
        public long getWindowSize() {
            return windowSize;
        }
        
        public long getMaxCount() {
            return maxCount;
        }
    }
}

3.3.4 短信發(fā)送核心服務(wù)

/**
 * 短信發(fā)送服務(wù)(核心業(yè)務(wù)邏輯)
 */
@Service
@Slf4j
public class SmsSendService {
    
    @Autowired
    private SlidingWindowRateLimiter slidingWindowRateLimiter;
    
    @Autowired
    private DistributedRateLimiter distributedRateLimiter;
    
    @Autowired
    private IpBlacklistService ipBlacklistService;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private SmsAsyncSender smsAsyncSender;
    
    // 驗(yàn)證碼有效期
    private static final long CODE_EXPIRE_SECONDS = 300;  // 5分鐘
    
    // 驗(yàn)證碼長度
    private static final int CODE_LENGTH = 6;
    
    // 黑名單閾值:單IP一天內(nèi)失敗超過50次則封禁
    private static final int BLACKLIST_THRESHOLD = 50;
    
    /**
     * 發(fā)送短信驗(yàn)證碼
     */
    public SmsResult sendSmsCode(String phone, String ip, String deviceId) {
        // ========== 第一層:黑名單檢查 ==========
        if (ipBlacklistService.isBlacklisted(ip)) {
            log.warn("IP在黑名單中,拒絕請(qǐng)求: ip={}", ip);
            return SmsResult.failed(ErrorCode.IP_BLOCKED);
        }
        
        // ========== 第二層:多維度限流檢查 ==========
        if (!checkRateLimit(phone, ip, deviceId)) {
            log.warn("觸發(fā)限流: phone={}, ip={}, deviceId={}", phone, ip, deviceId);
            return SmsResult.failed(ErrorCode.RATE_LIMIT);
        }
        
        // ========== 第三層:手機(jī)號(hào)格式驗(yàn)證 ==========
        if (!isValidPhone(phone)) {
            return SmsResult.failed(ErrorCode.INVALID_PHONE);
        }
        
        // ========== 第四層:生成驗(yàn)證碼并存儲(chǔ) ==========
        String code = generateRandomCode(CODE_LENGTH);
        String codeKey = RedisKeyConstant.SMS_CODE + phone;
        
        // 存儲(chǔ)驗(yàn)證碼
        redisTemplate.opsForValue().set(
            codeKey, 
            code, 
            CODE_EXPIRE_SECONDS, 
            TimeUnit.SECONDS
        );
        
        // ========== 第五層:異步發(fā)送短信 ==========
        try {
            smsAsyncSender.sendSmsAsync(phone, code, ip);
            
            // 記錄成功日志
            logSmsSend(phone, ip, deviceId, true, null);
            
            return SmsResult.success();
            
        } catch (Exception e) {
            log.error("短信發(fā)送失敗: phone={}", phone, e);
            
            // 記錄失敗日志
            logSmsSend(phone, ip, deviceId, false, e.getMessage());
            
            // 刪除已存儲(chǔ)的驗(yàn)證碼
            redisTemplate.delete(codeKey);
            
            return SmsResult.failed(ErrorCode.SEND_FAILED);
        }
    }
    
    /**
     * 多維度限流檢查
     */
    private boolean checkRateLimit(String phone, String ip, String deviceId) {
        long now = System.currentTimeMillis();
        
        // 1. IP維度限流
        String ipMinuteKey = LimitDimension.IP_PER_MINUTE.buildKey(ip);
        String ipHourKey = LimitDimension.IP_PER_HOUR.buildKey(ip);
        
        if (!slidingWindowRateLimiter.tryAcquire(
                ipMinuteKey, 
                LimitDimension.IP_PER_MINUTE.getWindowSize(), 
                LimitDimension.IP_PER_MINUTE.getMaxCount())) {
            return false;
        }
        
        if (!slidingWindowRateLimiter.tryAcquire(
                ipHourKey, 
                LimitDimension.IP_PER_HOUR.getWindowSize(), 
                LimitDimension.IP_PER_HOUR.getMaxCount())) {
            return false;
        }
        
        // 2. 手機(jī)號(hào)維度限流
        String phoneMinuteKey = LimitDimension.PHONE_PER_MINUTE.buildKey(phone);
        String phoneHourKey = LimitDimension.PHONE_PER_HOUR.buildKey(phone);
        String phoneDayKey = LimitDimension.PHONE_PER_DAY.buildKey(phone);
        
        if (!slidingWindowRateLimiter.tryAcquire(
                phoneMinuteKey, 
                LimitDimension.PHONE_PER_MINUTE.getWindowSize(), 
                LimitDimension.PHONE_PER_MINUTE.getMaxCount())) {
            return false;
        }
        
        if (!slidingWindowRateLimiter.tryAcquire(
                phoneHourKey, 
                LimitDimension.PHONE_PER_HOUR.getWindowSize(), 
                LimitDimension.PHONE_PER_HOUR.getMaxCount())) {
            return false;
        }
        
        if (!slidingWindowRateLimiter.tryAcquire(
                phoneDayKey, 
                LimitDimension.PHONE_PER_DAY.getWindowSize(), 
                LimitDimension.PHONE_PER_DAY.getMaxCount())) {
            return false;
        }
        
        // 3. 設(shè)備維度限流(如果有設(shè)備ID)
        if (StringUtils.isNotEmpty(deviceId)) {
            String deviceKey = "sms:limit:device:" + deviceId;
            if (!slidingWindowRateLimiter.tryAcquire(
                    deviceKey, 
                    60 * 60 * 1000,  // 1小時(shí)
                    20)) {             // 最多20次
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * 驗(yàn)證手機(jī)號(hào)格式
     */
    private boolean isValidPhone(String phone) {
        // 中國大陸手機(jī)號(hào)正則
        String regex = "^1[3-9]\\d{9}$";
        return StringUtils.isNotEmpty(phone) && phone.matches(regex);
    }
    
    /**
     * 生成隨機(jī)驗(yàn)證碼
     */
    private String generateRandomCode(int length) {
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(random.nextInt(10));
        }
        return sb.toString();
    }
    
    /**
     * 記錄短信發(fā)送日志
     */
    private void logSmsSend(String phone, String ip, String deviceId, 
                            boolean success, String errorMsg) {
        String logKey = RedisKeyConstant.SMS_SEND_LOG + 
                        DateUtil.format(new Date(), "yyyyMMdd");
        
        Map<String, String> logData = new HashMap<>();
        logData.put("phone", phone);
        logData.put("ip", ip);
        logData.put("deviceId", deviceId);
        logData.put("timestamp", String.valueOf(System.currentTimeMillis()));
        logData.put("success", String.valueOf(success));
        if (StringUtils.isNotEmpty(errorMsg)) {
            logData.put("errorMsg", errorMsg);
        }
        
        // 使用List結(jié)構(gòu)存儲(chǔ)日志
        redisTemplate.opsForList().rightPushAll(logKey, logData.toString());
        redisTemplate.expire(logKey, 7, TimeUnit.DAYS);
        
        // 檢查是否需要加入黑名單
        checkAndAddToBlacklist(ip);
    }
    
    /**
     * 檢查并加入黑名單
     */
    private void checkAndAddToBlacklist(String ip) {
        String failKey = RedisKeyConstant.SMS_FAIL_COUNT + ip;
        Long failCount = redisTemplate.opsForValue().increment(failKey);
        
        // 設(shè)置過期時(shí)間
        if (failCount == 1) {
            redisTemplate.expire(failKey, 1, TimeUnit.DAYS);
        }
        
        // 超過閾值則加入黑名單
        if (failCount >= BLACKLIST_THRESHOLD) {
            String reason = "單日短信發(fā)送失敗次數(shù)超過閾值";
            ipBlacklistService.addToBlacklist(ip, reason);
            log.warn("IP加入黑名單: ip={}, failCount={}", ip, failCount);
        }
    }
}

3.3.5 異步短信發(fā)送器

/**
 * 異步短信發(fā)送器
 */
@Component
@Slf4j
public class SmsAsyncSender {
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 異步發(fā)送短信(通過消息隊(duì)列)
     */
    @Async("smsExecutor")
    public void sendSmsAsync(String phone, String code, String ip) {
        try {
            // 構(gòu)建短信消息
            SmsMessage message = SmsMessage.builder()
                .phone(phone)
                .code(code)
                .ip(ip)
                .timestamp(System.currentTimeMillis())
                .retryCount(0)
                .build();
            
            // 發(fā)送到Kafka
            kafkaTemplate.send(
                "sms-send-topic", 
                phone,  // 使用手機(jī)號(hào)作為key,保證同一手機(jī)號(hào)的消息有序
                JSON.toJSONString(message)
            );
            
            log.info("短信消息已發(fā)送到Kafka: phone={}", phone);
            
        } catch (Exception e) {
            log.error("發(fā)送短信消息到Kafka失敗: phone={}", phone, e);
            throw new SmsSendException("消息隊(duì)列發(fā)送失敗", e);
        }
    }
}

/**
 * 短信消息消費(fèi)者
 */
@Component
@Slf4j
public class SmsMessageConsumer {
    
    @Autowired
    private SmsProvider smsProvider;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @KafkaListener(
        topics = "sms-send-topic",
        groupId = "sms-consumer-group",
        concurrency = "5"  // 5個(gè)并發(fā)消費(fèi)者
    )
    public void consumeSmsMessage(ConsumerRecord<String, String> record) {
        try {
            String messageJson = record.value();
            SmsMessage message = JSON.parseObject(messageJson, SmsMessage.class);
            
            // 冪等性檢查:防止重復(fù)消費(fèi)
            String idempotentKey = RedisKeyConstant.SMS_IDEMPOTENT + 
                                   message.getPhone() + ":" + 
                                   message.getTimestamp();
            
            if (Boolean.TRUE.equals(redisTemplate.hasKey(idempotentKey))) {
                log.warn("重復(fù)消費(fèi)消息: phone={}", message.getPhone());
                return;
            }
            
            // 調(diào)用短信服務(wù)商接口
            boolean success = smsProvider.sendSms(
                message.getPhone(), 
                buildSmsContent(message.getCode())
            );
            
            if (success) {
                // 標(biāo)記為已處理
                redisTemplate.opsForValue().set(
                    idempotentKey, 
                    "1", 
                    24, 
                    TimeUnit.HOURS
                );
                
                log.info("短信發(fā)送成功: phone={}", message.getPhone());
                
            } else {
                // 失敗重試
                handleFailure(message);
            }
            
        } catch (Exception e) {
            log.error("消費(fèi)短信消息失敗", e);
            throw e;  // 拋出異常觸發(fā)Kafka重試
        }
    }
    
    /**
     * 處理發(fā)送失敗
     */
    private void handleFailure(SmsMessage message) {
        int maxRetry = 3;
        
        if (message.getRetryCount() < maxRetry) {
            // 增加重試次數(shù)
            message.setRetryCount(message.getRetryCount() + 1);
            
            // 延遲重試(可以通過延遲消息隊(duì)列實(shí)現(xiàn))
            // 這里簡化為直接重新發(fā)送
            try {
                Thread.sleep(1000 * message.getRetryCount());  // 指數(shù)退避
                
                kafkaTemplate.send(
                    "sms-send-topic", 
                    message.getPhone(), 
                    JSON.toJSONString(message)
                );
                
            } catch (Exception e) {
                log.error("重試發(fā)送失敗: phone={}", message.getPhone(), e);
            }
            
        } else {
            log.error("短信發(fā)送重試次數(shù)超限: phone={}", message.getPhone());
            // 可以告警通知人工處理
        }
    }
    
    /**
     * 構(gòu)建短信內(nèi)容
     */
    private String buildSmsContent(String code) {
        return String.format("【您的驗(yàn)證碼】%s,5分鐘內(nèi)有效,請(qǐng)勿泄露給他人。", code);
    }
}

3.3.6 驗(yàn)證碼校驗(yàn)服務(wù)

/**
 * 驗(yàn)證碼校驗(yàn)服務(wù)
 */
@Service
@Slf4j
public class SmsCodeValidator {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 驗(yàn)證錯(cuò)誤次數(shù)限制
    private static final int MAX_VERIFY_FAIL_COUNT = 5;
    
    /**
     * 校驗(yàn)驗(yàn)證碼
     */
    public VerifyResult verifyCode(String phone, String code, String ip) {
        // ========== 第一層:檢查錯(cuò)誤次數(shù) ==========
        String failCountKey = RedisKeyConstant.VERIFY_FAIL_COUNT + phone;
        Integer failCount = (Integer) redisTemplate.opsForValue().get(failCountKey);
        
        if (failCount != null && failCount >= MAX_VERIFY_FAIL_COUNT) {
            return VerifyResult.failed(ErrorCode.VERIFY_TOO_MANY);
        }
        
        // ========== 第二層:從Redis獲取驗(yàn)證碼 ==========
        String codeKey = RedisKeyConstant.SMS_CODE + phone;
        String storedCode = redisTemplate.opsForValue().get(codeKey);
        
        if (StringUtils.isEmpty(storedCode)) {
            return VerifyResult.failed(ErrorCode.CODE_EXPIRED);
        }
        
        // ========== 第三層:比對(duì)驗(yàn)證碼 ==========
        if (!storedCode.equals(code)) {
            // 增加錯(cuò)誤次數(shù)
            redisTemplate.opsForValue().increment(failCountKey);
            redisTemplate.expire(failCountKey, 1, TimeUnit.HOURS);
            
            return VerifyResult.failed(ErrorCode.CODE_ERROR);
        }
        
        // ========== 第四層:驗(yàn)證通過,清理數(shù)據(jù) ==========
        redisTemplate.delete(codeKey);
        redisTemplate.delete(failCountKey);
        
        return VerifyResult.success();
    }
}

3.4 設(shè)備指紋識(shí)別

/**
 * 設(shè)備指紋生成器
 */
@Component
@Slf4j
public class DeviceFingerprintGenerator {
    
    private static final String DEVICE_FP_SALT = "your_salt_here";
    
    /**
     * 生成設(shè)備指紋
     */
    public String generateFingerprint(HttpServletRequest request) {
        try {
            // 1. 收集設(shè)備特征
            Map<String, String> features = collectDeviceFeatures(request);
            
            // 2. 排序特征
            TreeMap<String, String> sortedFeatures = new TreeMap<>(features);
            
            // 3. 拼接字符串
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : sortedFeatures.entrySet()) {
                sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
            sb.append("salt=").append(DEVICE_FP_SALT);
            
            // 4. 計(jì)算SHA256
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
            
            // 5. 轉(zhuǎn)換為十六進(jìn)制字符串(取前32位)
            String fingerprint = bytesToHex(hash).substring(0, 32);
            
            return fingerprint;
            
        } catch (Exception e) {
            log.error("生成設(shè)備指紋失敗", e);
            return UUID.randomUUID().toString();  // 降級(jí)處理
        }
    }
    
    /**
     * 收集設(shè)備特征
     */
    private Map<String, String> collectDeviceFeatures(HttpServletRequest request) {
        Map<String, String> features = new HashMap<>();
        
        // User-Agent
        features.put("ua", request.getHeader("User-Agent"));
        
        // Accept-Language
        features.put("lang", request.getHeader("Accept-Language"));
        
        // Accept-Encoding
        features.put("encoding", request.getHeader("Accept-Encoding"));
        
        // IP地址
        features.put("ip", getClientIp(request));
        
        // Screen分辨率(如果前端傳遞)
        String screen = request.getParameter("screen");
        if (StringUtils.isNotEmpty(screen)) {
            features.put("screen", screen);
        }
        
        // 時(shí)區(qū)(如果前端傳遞)
        String timezone = request.getParameter("timezone");
        if (StringUtils.isNotEmpty(timezone)) {
            features.put("timezone", timezone);
        }
        
        return features;
    }
    
    /**
     * 獲取客戶端真實(shí)IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        
        // 多級(jí)代理時(shí)取第一個(gè)IP
        if (StringUtils.isNotEmpty(ip) && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        
        return ip;
    }
    
    /**
     * 字節(jié)數(shù)組轉(zhuǎn)十六進(jìn)制字符串
     */
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

3.5 異常IP識(shí)別與自動(dòng)封禁

/**
 * 異常IP檢測服務(wù)
 */
@Component
@Slf4j
public class AbnormalIpDetector {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private IpBlacklistService ipBlacklistService;
    
    // 檢測窗口時(shí)間(1小時(shí))
    private static final long DETECTION_WINDOW = 60 * 60 * 1000;
    
    /**
     * 檢測異常IP并封禁
     */
    @Scheduled(fixedDelay = 5 * 60 * 1000)  // 每5分鐘執(zhí)行一次
    public void detectAndBlockAbnormalIps() {
        log.info("開始檢測異常IP...");
        
        // 1. 獲取所有IP的訪問數(shù)據(jù)
        Set<String> ips = getAllActiveIps();
        
        for (String ip : ips) {
            try {
                if (isAbnormalIp(ip)) {
                    blockIp(ip);
                }
            } catch (Exception e) {
                log.error("檢測IP異常: ip={}", ip, e);
            }
        }
        
        log.info("異常IP檢測完成");
    }
    
    /**
     * 獲取所有活躍IP
     */
    private Set<String> getAllActiveIps() {
        Set<String> ips = new HashSet<>();
        
        // 從Redis中獲取所有活躍IP
        Set<String> keys = redisTemplate.keys("sms:limit:ip:*");
        if (keys != null) {
            for (String key : keys) {
                // 提取IP地址
                String ip = key.substring("sms:limit:ip:".length());
                ips.add(ip);
            }
        }
        
        return ips;
    }
    
    /**
     * 判斷是否為異常IP
     */
    private boolean isAbnormalIp(String ip) {
        // 異常特征1:短時(shí)間內(nèi)請(qǐng)求大量不同手機(jī)號(hào)
        if (hasManyDifferentPhones(ip)) {
            log.warn("IP請(qǐng)求大量不同手機(jī)號(hào): ip={}", ip);
            return true;
        }
        
        // 異常特征2:驗(yàn)證碼驗(yàn)證失敗率極高
        if (hasHighFailRate(ip)) {
            log.warn("IP驗(yàn)證碼失敗率極高: ip={}", ip);
            return true;
        }
        
        // 異常特征3:請(qǐng)求時(shí)間分布過于均勻(疑似腳本)
        if (hasUniformRequestPattern(ip)) {
            log.warn("IP請(qǐng)求模式過于均勻: ip={}", ip);
            return true;
        }
        
        return false;
    }
    
    /**
     * 檢查是否請(qǐng)求大量不同手機(jī)號(hào)
     */
    private boolean hasManyDifferentPhones(String ip) {
        // 統(tǒng)計(jì)1小時(shí)內(nèi)該IP請(qǐng)求的不同手機(jī)號(hào)數(shù)量
        String pattern = "sms:*:phone:*";
        
        // 從日志中統(tǒng)計(jì)(簡化實(shí)現(xiàn))
        String logKey = RedisKeyConstant.SMS_SEND_LOG + 
                        DateUtil.format(new Date(), "yyyyMMdd");
        
        // 使用Lua腳本統(tǒng)計(jì)
        String luaScript =
            "local count = 0 " +
            "local phones = {} " +
            "for i, data in ipairs(redis.call('LRANGE', KEYS[1], 0, -1)) do " +
            "    local json = cjson.decode(data) " +
            "    if json.ip == ARGV[1] then " +
            "        phones[json.phone] = true " +
            "    end " +
            "end " +
            "for _ in pairs(phones) do " +
            "    count = count + 1 " +
            "end " +
            "return count";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long phoneCount = redisTemplate.execute(
            script,
            Collections.singletonList(logKey),
            ip
        );
        
        // 1小時(shí)內(nèi)請(qǐng)求超過50個(gè)不同手機(jī)號(hào)判定為異常
        return phoneCount != null && phoneCount > 50;
    }
    
    /**
     * 檢查驗(yàn)證失敗率是否過高
     */
    private boolean hasHighFailRate(String ip) {
        String failKey = RedisKeyConstant.VERIFY_FAIL_COUNT + ip;
        Integer failCount = (Integer) redisTemplate.opsForValue().get(failKey);
        
        if (failCount == null || failCount < 10) {
            return false;
        }
        
        // 獲取總請(qǐng)求數(shù)
        String totalKey = "sms:total:ip:" + ip;
        Long totalCount = redisTemplate.opsForValue().increment(totalKey, 0);
        
        if (totalCount == null) {
            return false;
        }
        
        // 失敗率超過80%判定為異常
        double failRate = (double) failCount / totalCount;
        return failRate > 0.8;
    }
    
    /**
     * 檢查請(qǐng)求模式是否過于均勻
     */
    private boolean hasUniformRequestPattern(String ip) {
        // 獲取該IP的請(qǐng)求時(shí)間戳列表
        String logKey = RedisKeyConstant.SMS_SEND_LOG + 
                        DateUtil.format(new Date(), "yyyyMMdd");
        
        // 使用Lua腳本提取時(shí)間戳
        String luaScript =
            "local timestamps = {} " +
            "for i, data in ipairs(redis.call('LRANGE', KEYS[1], 0, -1)) do " +
            "    local json = cjson.decode(data) " +
            "    if json.ip == ARGV[1] then " +
            "        table.insert(timestamps, json.timestamp) " +
            "    end " +
            "end " +
            "return timestamps";
        
        DefaultRedisScript<List> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(List.class);
        
        List<Long> timestamps = redisTemplate.execute(
            script,
            Collections.singletonList(logKey),
            ip
        );
        
        if (timestamps == null || timestamps.size() < 10) {
            return false;
        }
        
        // 計(jì)算時(shí)間間隔的標(biāo)準(zhǔn)差
        double stdDev = calculateStandardDeviation(timestamps);
        
        // 標(biāo)準(zhǔn)差小于100ms判定為異常(過于均勻)
        return stdDev < 100;
    }
    
    /**
     * 計(jì)算標(biāo)準(zhǔn)差
     */
    private double calculateStandardDeviation(List<Long> timestamps) {
        // 計(jì)算時(shí)間間隔
        List<Long> intervals = new ArrayList<>();
        for (int i = 1; i < timestamps.size(); i++) {
            intervals.add(timestamps.get(i) - timestamps.get(i - 1));
        }
        
        // 計(jì)算平均值
        double mean = intervals.stream()
            .mapToLong(Long::longValue)
            .average()
            .orElse(0);
        
        // 計(jì)算方差
        double variance = intervals.stream()
            .mapToDouble(interval -> Math.pow(interval - mean, 2))
            .average()
            .orElse(0);
        
        // 標(biāo)準(zhǔn)差
        return Math.sqrt(variance);
    }
    
    /**
     * 封禁IP
     */
    private void blockIp(String ip) {
        String reason = "檢測到異常行為";
        ipBlacklistService.addToBlacklist(ip, reason);
        log.warn("IP已自動(dòng)封禁: ip={}, reason={}", ip, reason);
        
        // 可以發(fā)送告警通知
        sendAlert(ip, reason);
    }
    
    /**
     * 發(fā)送告警
     */
    private void sendAlert(String ip, String reason) {
        // 實(shí)現(xiàn)告警邏輯(郵件、釘釘、企業(yè)微信等)
        log.info("發(fā)送告警: ip={}, reason={}", ip, reason);
    }
}

四、性能優(yōu)化策略

4.1 Redis性能優(yōu)化

4.1.1 Pipeline批量操作

/**
 * Redis Pipeline批量操作工具
 */
@Component
public class RedisPipelineHelper {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 批量獲取多個(gè)key的值
     */
    public Map<String, String> multiGet(Collection<String> keys) {
        if (keys == null || keys.isEmpty()) {
            return Collections.emptyMap();
        }
        
        return redisTemplate.executePipelined(
            (RedisCallback<Object>) connection -> {
                for (String key : keys) {
                    connection.get(key.getBytes());
                }
                return null;
            }
        ).stream()
         .filter(Objects::nonNull)
         .map(obj -> (String) obj)
         .collect(Collectors.toMap(
             value -> extractKey(value, keys),  // 需要實(shí)現(xiàn)key提取邏輯
             value -> value
         ));
    }
    
    /**
     * 批量設(shè)置多個(gè)key的值
     */
    public void multiSet(Map<String, String> data, long expireSeconds) {
        if (data == null || data.isEmpty()) {
            return;
        }
        
        redisTemplate.executePipelined(
            (RedisCallback<Object>) connection -> {
                for (Map.Entry<String, String> entry : data.entrySet()) {
                    byte[] key = entry.getKey().getBytes();
                    byte[] value = entry.getValue().getBytes();
                    
                    connection.set(key, value);
                    connection.expire(key, expireSeconds);
                }
                return null;
            }
        );
    }
}

4.1.2 本地緩存二級(jí)緩存

/**
 * 本地緩存 + Redis二級(jí)緩存
 */
@Component
@Slf4j
public class TwoLevelCache {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 本地緩存(使用Caffeine)
    private final Cache<String, String> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(10, TimeUnit.SECONDS)  // 本地緩存10秒
        .build();
    
    /**
     * 獲取緩存值
     */
    public String get(String key) {
        // 第一層:本地緩存
        String value = localCache.getIfPresent(key);
        if (value != null) {
            log.debug("本地緩存命中: key={}", key);
            return value;
        }
        
        // 第二層:Redis緩存
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            log.debug("Redis緩存命中: key={}", key);
            // 回填本地緩存
            localCache.put(key, value);
        }
        
        return value;
    }
    
    /**
     * 設(shè)置緩存值
     */
    public void put(String key, String value, long expireSeconds) {
        // 同時(shí)設(shè)置本地緩存和Redis
        localCache.put(key, value);
        redisTemplate.opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
    }
    
    /**
     * 刪除緩存值
     */
    public void delete(String key) {
        localCache.invalidate(key);
        redisTemplate.delete(key);
    }
    
    /**
     * 批量獲取
     */
    public Map<String, String> multiGet(Collection<String> keys) {
        Map<String, String> result = new HashMap<>();
        
        // 先從本地緩存獲取
        Set<String> remainingKeys = new HashSet<>();
        for (String key : keys) {
            String value = localCache.getIfPresent(key);
            if (value != null) {
                result.put(key, value);
            } else {
                remainingKeys.add(key);
            }
        }
        
        // 剩余的key從Redis獲取
        if (!remainingKeys.isEmpty()) {
            List<String> redisValues = redisTemplate.opsForValue()
                .multiGet(remainingKeys);
            
            if (redisValues != null) {
                int index = 0;
                for (String key : remainingKeys) {
                    String value = redisValues.get(index++);
                    if (value != null) {
                        result.put(key, value);
                        localCache.put(key, value);
                    }
                }
            }
        }
        
        return result;
    }
}

4.1.3 Redis集群優(yōu)化配置

# application.yml
spring:
  redis:
    cluster:
      nodes:
        - redis-node1:6379
        - redis-node2:6379
        - redis-node3:6379
        - redis-node4:6379
        - redis-node5:6379
        - redis-node6:6379
      max-redirects: 3
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 10
        max-wait: 1000ms
    timeout: 2000ms

4.2 異步化優(yōu)化

/**
 * 短信異步發(fā)送配置
 */
@Configuration
@EnableAsync
public class SmsAsyncConfig {
    
    /**
     * 短信發(fā)送線程池
     */
    @Bean("smsExecutor")
    public Executor smsExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // 核心線程數(shù)(根據(jù)CPU核心數(shù)和任務(wù)類型調(diào)整)
        executor.setCorePoolSize(10);
        
        // 最大線程數(shù)
        executor.setMaxPoolSize(50);
        
        // 隊(duì)列容量
        executor.setQueueCapacity(500);
        
        // 線程名稱前綴
        executor.setThreadNamePrefix("sms-sender-");
        
        // 拒絕策略:調(diào)用者運(yùn)行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        
        // 線程空閑時(shí)間
        executor.setKeepAliveSeconds(60);
        
        // 允許核心線程超時(shí)
        executor.setAllowCoreThreadTimeOut(true);
        
        // 等待任務(wù)完成后關(guān)閉
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        
        executor.initialize();
        return executor;
    }
}

4.3 數(shù)據(jù)庫優(yōu)化

/**
 * 短信發(fā)送日志批量寫入
 */
@Service
@Slf4j
public class SmsLogBatchWriter {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 批量寫入閾值
    private static final int BATCH_SIZE = 100;
    
    // 批量寫入間隔(毫秒)
    private static final long FLUSH_INTERVAL = 5000;
    
    // 待寫入的數(shù)據(jù)緩存
    private final List<SmsLog> pendingLogs = new ArrayList<>(BATCH_SIZE);
    
    // 上次刷新時(shí)間
    private long lastFlushTime = System.currentTimeMillis();
    
    /**
     * 添加日志(線程安全)
     */
    @Async
    public synchronized void addLog(SmsLog log) {
        pendingLogs.add(log);
        
        long now = System.currentTimeMillis();
        
        // 達(dá)到批量大小或超時(shí)則刷新
        if (pendingLogs.size() >= BATCH_SIZE || 
            now - lastFlushTime > FLUSH_INTERVAL) {
            flush();
        }
    }
    
    /**
     * 刷新數(shù)據(jù)到數(shù)據(jù)庫
     */
    private synchronized void flush() {
        if (pendingLogs.isEmpty()) {
            return;
        }
        
        try {
            // 批量插入
            String sql = "INSERT INTO sms_log (phone, ip, device_id, status, " +
                        "error_msg, created_at) VALUES (?, ?, ?, ?, ?, ?)";
            
            jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    SmsLog log = pendingLogs.get(i);
                    ps.setString(1, log.getPhone());
                    ps.setString(2, log.getIp());
                    ps.setString(3, log.getDeviceId());
                    ps.setInt(4, log.getStatus());
                    ps.setString(5, log.getErrorMsg());
                    ps.setTimestamp(6, new Timestamp(log.getCreatedAt()));
                }
                
                @Override
                public int getBatchSize() {
                    return pendingLogs.size();
                }
            });
            
            log.info("批量寫入短信日志成功: count={}", pendingLogs.size());
            
        } catch (Exception e) {
            log.error("批量寫入短信日志失敗", e);
        } finally {
            pendingLogs.clear();
            lastFlushTime = System.currentTimeMillis();
        }
    }
    
    /**
     * 定時(shí)刷新(防止數(shù)據(jù)積壓)
     */
    @Scheduled(fixedDelay = FLUSH_INTERVAL)
    public void scheduledFlush() {
        flush();
    }
}

4.4 JVM優(yōu)化建議

# JVM啟動(dòng)參數(shù)建議(針對(duì)高并發(fā)場景)
java -Xms4g -Xmx4g \
     -XX:NewRatio=1 \
     -XX:SurvivorRatio=8 \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:G1HeapRegionSize=16m \
     -XX:ParallelGCThreads=4 \
     -XX:ConcGCThreads=2 \
     -XX:+DisableExplicitGC \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/opt/logs/heap_dump.hprof \
     -Duser.timezone=Asia/Shanghai \
     -jar your-application.jar

五、監(jiān)控與告警

5.1 關(guān)鍵指標(biāo)監(jiān)控

/**
 * 短信服務(wù)指標(biāo)監(jiān)控
 */
@Component
@Slf4j
public class SmsMetricsMonitor {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    /**
     * 記錄短信發(fā)送指標(biāo)
     */
    public void recordSmsSend(String phone, boolean success, long costTime) {
        // 1. 計(jì)數(shù)器
        meterRegistry.counter(
            "sms.send.count",
            "success", String.valueOf(success)
        ).increment();
        
        // 2. 耗時(shí)分布
        meterRegistry.timer("sms.send.time").record(costTime, TimeUnit.MILLISECONDS);
        
        // 3. 手機(jī)號(hào)分布
        meterRegistry.counter(
            "sms.send.by.phone",
            "prefix", phone.substring(0, 3)
        ).increment();
        
        // 4. 成功率
        if (success) {
            meterRegistry.gauge("sms.send.success.rate", 1.0);
        }
    }
    
    /**
     * 記錄限流指標(biāo)
     */
    public void recordRateLimit(String dimension, String identifier) {
        meterRegistry.counter(
            "sms.rate.limit",
            "dimension", dimension
        ).increment();
    }
    
    /**
     * 記錄黑名單攔截
     */
    public void recordBlacklistBlock(String ip) {
        meterRegistry.counter(
            "sms.blacklist.block",
            "ip", ip
        ).increment();
    }
}

5.2 Prometheus + Grafana監(jiān)控面板

關(guān)鍵監(jiān)控指標(biāo)

指標(biāo)名稱類型說明告警閾值
sms.send.countCounter短信發(fā)送總數(shù)-
sms.send.success.rateGauge發(fā)送成功率< 95%
sms.send.timeHistogram發(fā)送耗時(shí)分布P99 > 5s
sms.rate.limitCounter限流次數(shù)> 100/min
sms.blacklist.blockCounter黑名單攔截次數(shù)> 50/min
redis.slowlog.countCounterRedis慢查詢> 10/min

Grafana面板查詢示例

# 短信發(fā)送成功率
sum(rate(sms_send_count{success="true"}[5m])) / sum(rate(sms_send_count[5m])) * 100

# 發(fā)送耗時(shí)P99
histogram_quantile(0.99, rate(sms_send_time_bucket[5m]))

# 限流攔截趨勢(shì)
sum(rate(sms_rate_limit[5m])) by (dimension)

# 黑名單攔截Top10 IP
topk(10, sum(rate(sms_blacklist_block[5m])) by (ip))

5.3 告警規(guī)則配置

# alert-rules.yml
groups:
  - name: sms_alerts
    rules:
      # 短信發(fā)送成功率過低
      - alert: SmsSuccessRateLow
        expr: |
          sum(rate(sms_send_count{success="true"}[5m])) / 
          sum(rate(sms_send_count[5m])) * 100 < 95
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "短信發(fā)送成功率過低"
          description: "最近5分鐘短信發(fā)送成功率為 {{ $value }}%"
      # 發(fā)送耗時(shí)過高
      - alert: SmsSendTimeHigh
        expr: |
          histogram_quantile(0.99, rate(sms_send_time_bucket[5m])) > 5000
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "短信發(fā)送耗時(shí)過高"
          description: "P99耗時(shí)為 {{ $value }}ms"
      # 限流攔截過多
      - alert: SmsRateLimitHigh
        expr: |
          sum(rate(sms_rate_limit[5m])) > 100
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "限流攔截過多"
          description: "每分鐘限流攔截 {{ $value }} 次"
      # Redis慢查詢過多
      - alert: RedisSlowlogHigh
        expr: |
          rate(redis_slowlog_count[5m]) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis慢查詢過多"
          description: "每分鐘 {{ $value }} 個(gè)慢查詢"

六、高并發(fā)場景下的特殊處理

6.1 緩存擊穿防護(hù)

/**
 * 緩存擊穿防護(hù)(互斥鎖 + 邏輯過期)
 */
@Component
public class CacheBreakdownProtection {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 鎖的過期時(shí)間
    private static final long LOCK_EXPIRE_SECONDS = 10;
    
    /**
     * 獲取緩存數(shù)據(jù)(防止擊穿)
     */
    public <T> T getWithLock(String key, Class<T> type, 
                           Supplier<T> dataLoader, 
                           long expireSeconds) {
        // 1. 先從緩存獲取
        String value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return JSON.parseObject(value, type);
        }
        
        // 2. 獲取分布式鎖
        String lockKey = "lock:" + key;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            // 嘗試獲取鎖
            Boolean locked = redisTemplate.opsForValue().setIfAbsent(
                lockKey, 
                lockValue, 
                LOCK_EXPIRE_SECONDS, 
                TimeUnit.SECONDS
            );
            
            if (Boolean.TRUE.equals(locked)) {
                // 獲取鎖成功,加載數(shù)據(jù)
                T data = dataLoader.get();
                
                // 存入緩存
                if (data != null) {
                    redisTemplate.opsForValue().set(
                        key, 
                        JSON.toJSONString(data), 
                        expireSeconds, 
                        TimeUnit.SECONDS
                    );
                }
                
                return data;
                
            } else {
                // 獲取鎖失敗,短暫休眠后重試
                Thread.sleep(50);
                return getWithLock(key, type, dataLoader, expireSeconds);
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            // 釋放鎖(使用Lua腳本保證原子性)
            if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                              "return redis.call('del', KEYS[1]) else return 0 end";
                redisTemplate.execute(
                    new DefaultRedisScript<>(script, Long.class),
                    Collections.singletonList(lockKey),
                    lockValue
                );
            }
        }
    }
}

6.2 緩存雪崩防護(hù)

/**
 * 緩存雪崩防護(hù)(隨機(jī)過期時(shí)間)
 */
@Component
public class CacheAvalancheProtection {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private final Random random = new Random();
    
    /**
     * 設(shè)置緩存(帶隨機(jī)過期時(shí)間)
     */
    public void setWithRandomExpire(String key, String value, 
                                    long baseExpireSeconds) {
        // 添加隨機(jī)時(shí)間(0-300秒)
        long randomExpire = random.nextInt(300);
        long totalExpire = baseExpireSeconds + randomExpire;
        
        redisTemplate.opsForValue().set(
            key, 
            value, 
            totalExpire, 
            TimeUnit.SECONDS
        );
    }
    
    /**
     * 緩存預(yù)熱(啟動(dòng)時(shí)加載熱點(diǎn)數(shù)據(jù))
     */
    @PostConstruct
    public void preloadHotData() {
        log.info("開始預(yù)熱緩存...");
        
        // 預(yù)熱限流配置
        preloadRateLimitConfig();
        
        // 預(yù)熱黑名單
        preloadBlacklist();
        
        log.info("緩存預(yù)熱完成");
    }
    
    /**
     * 預(yù)熱限流配置
     */
    private void preloadRateLimitConfig() {
        // 預(yù)加載常用的限流鍵到本地緩存
        // 避免剛啟動(dòng)時(shí)大量請(qǐng)求穿透到Redis
    }
    
    /**
     * 預(yù)熱黑名單
     */
    private void preloadBlacklist() {
        // 從數(shù)據(jù)庫加載黑名單到Redis
    }
}

6.3 熱點(diǎn)數(shù)據(jù)限流

/**
 * 熱點(diǎn)數(shù)據(jù)識(shí)別與特殊限流
 */
@Component
public class HotspotDataLimiter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 檢查是否為熱點(diǎn)數(shù)據(jù)
     */
    public boolean isHotspot(String key) {
        String hotspotKey = "hotspot:" + key;
        
        // 增加訪問計(jì)數(shù)
        Long count = redisTemplate.opsForValue().increment(hotspotKey);
        
        // 設(shè)置過期時(shí)間(1分鐘)
        if (count == 1) {
            redisTemplate.expire(hotspotKey, 1, TimeUnit.MINUTES);
        }
        
        // 1分鐘內(nèi)訪問超過100次判定為熱點(diǎn)
        return count != null && count > 100;
    }
    
    /**
     * 熱點(diǎn)數(shù)據(jù)限流檢查
     */
    public boolean checkHotspotLimit(String key) {
        if (!isHotspot(key)) {
            return true;  // 不是熱點(diǎn),不限流
        }
        
        // 熱點(diǎn)數(shù)據(jù)使用更嚴(yán)格的限流
        String limitKey = "hotspot:limit:" + key;
        
        // 使用令牌桶算法,更小的桶容量
        // 每秒最多10個(gè)請(qǐng)求
        return redisTemplate.opsForValue().setIfAbsent(
            limitKey,
            "1",
            100,
            TimeUnit.MILLISECONDS
        ) != null;
    }
}

七、完整流程總結(jié)

7.1 短信發(fā)送完整流程

用戶請(qǐng)求
    │
    ▼
前端校驗(yàn)(倒計(jì)時(shí)、圖形驗(yàn)證)
    │
    ▼
簽名驗(yàn)證
    │
    ▼
網(wǎng)關(guān)限流
    │
    ▼
IP黑名單檢查
    │
    ▼
多維度限流(IP、手機(jī)號(hào)、設(shè)備)
    │
    ▼
設(shè)備指紋識(shí)別
    │
    ▼
異常行為檢測
    │
    ▼
生成驗(yàn)證碼
    │
    ▼
Redis存儲(chǔ)
    │
    ▼
Kafka異步發(fā)送
    │
    ▼
短信服務(wù)商
    │
    ▼
更新狀態(tài) & 記錄日志
    │
    ▼
返回結(jié)果

7.2 驗(yàn)證碼校驗(yàn)完整流程

用戶提交驗(yàn)證碼
    │
    ▼
參數(shù)校驗(yàn)
    │
    ▼
錯(cuò)誤次數(shù)檢查
    │
    ▼
Redis獲取存儲(chǔ)的驗(yàn)證碼
    │
    ▼
比對(duì)驗(yàn)證碼
    │
    ▼
更新錯(cuò)誤次數(shù)(如果失?。?
    │
    ▼
刪除驗(yàn)證碼(如果成功)
    │
    ▼
返回結(jié)果

八、總結(jié)與建議

8.1 核心要點(diǎn)總結(jié)

  1. 分層防護(hù):從前端到后端,每層都要有防護(hù)措施
  2. 多維度限流:IP、手機(jī)號(hào)、設(shè)備三個(gè)維度結(jié)合
  3. 異步化處理:短信發(fā)送走消息隊(duì)列,不阻塞主流程
  4. 監(jiān)控告警:實(shí)時(shí)監(jiān)控關(guān)鍵指標(biāo),及時(shí)發(fā)現(xiàn)問題
  5. 動(dòng)態(tài)調(diào)整:根據(jù)攻擊模式動(dòng)態(tài)調(diào)整限流策略

8.2 性能優(yōu)化要點(diǎn)

優(yōu)化點(diǎn)優(yōu)化手段預(yù)期效果
Redis性能Pipeline、本地緩存、集群部署降低延遲50%+
異步處理Kafka、線程池提升吞吐量5-10倍
限流算法令牌桶、滑動(dòng)窗口平滑流量,保護(hù)后端
數(shù)據(jù)庫批量寫入、異步寫入減少DB壓力
JVM調(diào)優(yōu)G1GC、堆內(nèi)存配置降低GC停頓

8.3 部署架構(gòu)建議

                    ┌─────────────────┐
                    │     CDN/WAF     │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │   Nginx集群     │
                    │   (4核8G * 3)   │
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
┌───────▼───────┐  ┌────────▼────────┐  ┌───────▼───────┐
│  應(yīng)用節(jié)點(diǎn)1     │  │  應(yīng)用節(jié)點(diǎn)2      │  │  應(yīng)用節(jié)點(diǎn)3     │
│ (8核16G)      │  │  (8核16G)       │  │ (8核16G)      │
└───────┬───────┘  └────────┬────────┘  └───────┬───────┘
        │                    │                    │
        └────────────────────┼────────────────────┘
                             │
                    ┌────────▼────────┐
                    │ Redis Cluster  │
                    │ (主從 * 6)     │
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
┌───────▼───────┐  ┌────────▼────────┐  ┌───────▼───────┐
│   Kafka       │  │   MySQL集群     │  │  監(jiān)控系統(tǒng)      │
│   (3節(jié)點(diǎn))     │  │   (主從)        │  │  (Prometheus) │
└───────────────┘  └─────────────────┘  └───────────────┘

8.4 運(yùn)維建議

  1. 定期演練:每月進(jìn)行一次壓力測試和故障演練
  2. 預(yù)案準(zhǔn)備:制定DDoS攻擊、短信服務(wù)商故障等應(yīng)急預(yù)案
  3. 日志審計(jì):定期審計(jì)短信發(fā)送日志,發(fā)現(xiàn)潛在風(fēng)險(xiǎn)
  4. 成本控制:監(jiān)控短信成本,設(shè)置成本告警閾值
  5. 安全更新:及時(shí)更新限流規(guī)則和黑名單

九、附錄

9.1 常見問題FAQ

Q1:如何區(qū)分正常用戶和攻擊者?

A:通過多維度數(shù)據(jù)分析,包括:

  • 請(qǐng)求頻率(正常用戶不會(huì)高頻請(qǐng)求)
  • 手機(jī)號(hào)分布(攻擊者會(huì)使用大量不同手機(jī)號(hào))
  • 行為模式(腳本的時(shí)間間隔過于均勻)
  • 驗(yàn)證失敗率(攻擊者失敗率通常很高)

Q2:限流閾值如何設(shè)定?

A:根據(jù)業(yè)務(wù)特點(diǎn)和歷史數(shù)據(jù)分析:

  • 從保守值開始,逐步放開
  • 監(jiān)控限流攔截率,控制在5%以內(nèi)
  • 不同時(shí)間段可以使用不同閾值

Q3:如何處理短信服務(wù)商限流?

A:

  • 使用多個(gè)短信服務(wù)商作為備份
  • 實(shí)現(xiàn)智能路由,根據(jù)服務(wù)商狀態(tài)自動(dòng)切換
  • 本地緩存發(fā)送隊(duì)列,服務(wù)商恢復(fù)后重發(fā)

Q4:驗(yàn)證碼過期時(shí)間多長合適?

A:

  • 一般場景:5-10分鐘
  • 金融場景:3-5分鐘
  • 避免過長,增加安全風(fēng)險(xiǎn)

Q5:如何防止驗(yàn)證碼被泄露?

A:

  • 限制同一驗(yàn)證碼只能使用一次
  • 驗(yàn)證后立即刪除
  • 監(jiān)控異常的驗(yàn)證行為(如同一IP驗(yàn)證多個(gè)手機(jī)號(hào))

以上就是SpringBoot實(shí)現(xiàn)短信驗(yàn)證碼接口防刷的完整方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot短信驗(yàn)證碼接口防刷的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 基于springboot創(chuàng)建mybatis的完整步驟

    基于springboot創(chuàng)建mybatis的完整步驟

    MyBatis是一款優(yōu)秀的數(shù)據(jù)庫持久層框架,相比Hibernate我更喜歡使用MyBatis,看的到SQL還是讓人更安心點(diǎn),這篇文章主要給大家介紹了關(guān)于基于springboot創(chuàng)建mybatis的完整步驟,需要的朋友可以參考下
    2024-03-03
  • Spring實(shí)戰(zhàn)之搜索Bean類操作示例

    Spring實(shí)戰(zhàn)之搜索Bean類操作示例

    這篇文章主要介紹了Spring實(shí)戰(zhàn)之搜索Bean類操作,結(jié)合實(shí)例形式分析了Spring搜索Bean類的相關(guān)配置、接口實(shí)現(xiàn)與操作技巧,需要的朋友可以參考下
    2019-12-12
  • Java中ReentrantLock的用法和原理

    Java中ReentrantLock的用法和原理

    本文主要介紹了Java中ReentrantLock的用法和原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • Java泛型模擬scala實(shí)現(xiàn)自定義ArrayList方式

    Java泛型模擬scala實(shí)現(xiàn)自定義ArrayList方式

    這篇文章主要介紹了Java泛型模擬scala實(shí)現(xiàn)自定義ArrayList方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • Spring Java-based容器配置詳解

    Spring Java-based容器配置詳解

    這篇文章主要介紹了Spring Java-based容器配置詳解,涉及注解和@Configuration類以及@Beans的相關(guān)知識(shí),具有一定參考價(jià)值,需要的朋友可以了解。
    2017-10-10
  • Spring Cloud Alibaba教程之Sentinel的使用

    Spring Cloud Alibaba教程之Sentinel的使用

    這篇文章主要介紹了Spring Cloud Alibaba教程之Sentinel的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • springboot2?使用activiti6?idea插件的過程詳解

    springboot2?使用activiti6?idea插件的過程詳解

    這篇文章主要介紹了springboot2?使用activiti6?idea插件,本文通過截圖實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-03-03
  • Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例

    Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例

    這篇文章主要介紹了Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-11-11
  • Java NIO實(shí)例UDP發(fā)送接收數(shù)據(jù)代碼分享

    Java NIO實(shí)例UDP發(fā)送接收數(shù)據(jù)代碼分享

    這篇文章主要介紹了Java NIO實(shí)例UDP發(fā)送接收數(shù)據(jù)代碼分享,分享了客戶端和服務(wù)端完整代碼,小編覺得還是挺不錯(cuò)的,共需要的朋友參考。
    2017-11-11
  • SpringBoot使用RestTemplate如何通過http請(qǐng)求將文件下載到本地

    SpringBoot使用RestTemplate如何通過http請(qǐng)求將文件下載到本地

    文章介紹了如何通過編寫代碼批量下載文件,解決了沒有提供批量下載接口的問題,首先篩選出需要下載的文件ID,然后通過后端代碼發(fā)起HTTP請(qǐng)求,將下載的資源寫入本地文件中,總結(jié)了實(shí)現(xiàn)方式和注意事項(xiàng),希望能為類似任務(wù)提供參考
    2025-02-02

最新評(píng)論

射阳县| 禹城市| 江油市| 南平市| 轮台县| 清涧县| 保靖县| 太康县| 东海县| 新干县| 德惠市| 中卫市| 神农架林区| 长兴县| SHOW| 南部县| 南丹县| 河南省| 卢湾区| 长武县| 崇阳县| 镇赉县| 赣州市| 航空| 建始县| 滨州市| 高密市| 开江县| 唐海县| 新余市| 兖州市| 海盐县| 台北县| 襄垣县| 温州市| 昔阳县| 黎平县| 清丰县| 泗洪县| 抚宁县| 绥中县|