文章目录
-
- 前言
- 第一层:鉴权------别让接口"裸奔"
-
- [为什么API Key像你家钥匙?](#为什么API Key像你家钥匙?)
- [实战方案:JWT + 签名验证](#实战方案:JWT + 签名验证)
- 第二层:限流------给接口装上"红绿灯"
-
- 为什么AI接口特别需要限流?
- [算法选择:令牌桶 vs 漏桶](#算法选择:令牌桶 vs 漏桶)
- [Redis + Lua实现分布式限流](#Redis + Lua实现分布式限流)
- 第三层:高并发架构------别一压就垮
- 总结:安全是门"成本艺术"
无意间发现了一个CSDN大神的人工智能教程,忍不住分享一下给大家。很通俗易懂,重点是还非常风趣幽默,像看小说一样。床送门放这了👉 http://blog.csdn.net/jiangjunshow
前言
今年的315晚会看了没?那场面,简直是AI乱象的"大型处刑现场"。从投毒训练数据到虚假推荐算法,黑产团伙们已经把AI当成了新的"流量提款机"。
而你,作为一个辛辛苦苦的Java后端仔,可能还没意识到:你部署的那个AI问答接口,说不定正被某个脚本小子用十台肉鸡疯狂刷着。你的OpenAI API Key可能正在某个灰色论坛里被标价出售,你的阿里云灵积模型调用额度可能正在被批量薅羊毛。
这事儿我亲历过。去年有个朋友做了个AI文案生成的小工具,上线三天,账单直接飙到两万四。查日志发现,有人用分布式脚本在凌晨三点到五点之间,以每秒200次的频率狂调他的接口。关键是,这老哥连基础鉴权都没做,接口赤裸裸地暴露在公网上,简直是在邀请别人来"零元购"。
所以这篇文章,咱们就聊聊怎么给你的AI接口套上三层护甲:鉴权(证明你是你)、限流(防止你疯)、高并发(撑住场面)。全程Java实战,代码可直接抄作业。
第一层:鉴权------别让接口"裸奔"
为什么API Key像你家钥匙?
很多初学者写AI接口,直接就是@PostMapping("/chat"),参数里塞个prompt字符串,返回AI结果。这相当于你家大门不上锁,谁都能推门进来喝茶。
现在的黑产工具链已经产业化了。他们有个东西叫"API Key爆破器",其实就是拿着字典批量试接口。你的接口一旦响应里带点儿AI模型的特征(比如特定的返回格式、延迟特征),马上就会被标记为"可用目标"。
实战方案:JWT + 签名验证
别再用简单的Header传个api-key=123456了,这玩意儿被抓包就是秒破解。2026年的主流做法是非对称加密签名。
java
@RestController
@RequestMapping("/api/v1/ai")
public class AIController {
@PostMapping("/chat")
public ResponseEntity chat(@RequestBody ChatRequest request,
@RequestHeader("X-Signature") String signature,
@RequestHeader("X-Timestamp") Long timestamp) {
// 1. 时间戳防重放攻击(5分钟内有效)
if (Math.abs(System.currentTimeMillis() - timestamp) > 300000) {
return ResponseEntity.status(403).body("请求已过期");
}
// 2. 验签:用客户端公钥验证RSA签名
boolean valid = RSAUtil.verify(request.toString(), signature, clientPublicKey);
if (!valid) {
return ResponseEntity.status(403).body("签名无效");
}
// 3. 业务处理...
return ResponseEntity.ok(aiService.chat(request));
}
}
这里的关键是时间戳+签名的组合拳。即使攻击者截获了你的请求包,他没法伪造签名(没有私钥),也没法重放请求(时间戳过期就失效)。
当然,如果你觉得RSA太重,也可以用Sa-Token这个国产框架(2026年已经是v1.40+版本了),对JWT的支持非常丝滑,几行代码就能搞定登录鉴权:
java
@SaCheckLogin
@PostMapping("/secure-chat")
public SaResult secureChat(@RequestBody PromptDTO dto) {
// 自动校验登录态,非法请求直接拦在外面
return SaResult.ok(aiClient.generate(dto.getContent()));
}
第二层:限流------给接口装上"红绿灯"
为什么AI接口特别需要限流?
AI调用跟普通接口不一样,它是真·烧钱。一次GPT-4级别的调用,成本可能是普通REST接口的1000倍。如果被刷,财务风险极高。
而且AI模型本身有并发上限。比如某国产大模型可能只支持每秒10次并发,你一不限流,第11个请求就直接超时或报错,正常用户全被挤走。
算法选择:令牌桶 vs 漏桶
这俩算法就像两种保安:
- 漏桶算法:不管外面来多少人,只按固定频率放人(匀速处理)。适合需要严格平滑流量的场景,但突发流量会被削平。
- 令牌桶算法:桶里先放一堆令牌,有令牌的直接进,没令牌的排队或拒绝。可以应对突发流量(只要桶里还有令牌)。
对于AI接口,令牌桶更合适。因为用户高峰期可能突然涌入(比如早上9点大家上班提需求),令牌桶允许一定程度的突发,不像漏桶那么死板。
Redis + Lua实现分布式限流
单应用限流用Guava RateLimiter就行,但生产环境都是集群部署,得用Redis做全局计数器。Lua脚本保证原子性,防止竞态条件。
java
@Component
public class AILimiter {
@Autowired
private StringRedisTemplate redisTemplate;
// Lua脚本:令牌桶核心逻辑
private static final String LUA_SCRIPT =
"local key = KEYS[1] " +
"local rate = tonumber(ARGV[1]) " + // 每秒产生令牌数
"local capacity = tonumber(ARGV[2]) " + // 桶容量
"local now = tonumber(ARGV[3]) " +
"local requested = tonumber(ARGV[4]) " +
"local fill_time = capacity / rate " +
"local ttl = math.floor(fill_time * 2) " +
"local last_tokens = redis.call('get', key) " +
"if last_tokens == false then " +
" last_tokens = capacity " +
"end " +
"local last_updated = redis.call('get', key .. ':last_refreshed') " +
"if last_updated == false then " +
" last_updated = 0 " +
"end " +
"local delta = math.max(0, now - tonumber(last_updated)) " +
"local filled_tokens = math.min(capacity, last_tokens + (delta * rate)) " +
"local allowed = filled_tokens >= requested " +
"local new_tokens = filled_tokens " +
"if allowed then " +
" new_tokens = filled_tokens - requested " +
"end " +
"redis.call('setex', key, ttl, new_tokens) " +
"redis.call('setex', key .. ':last_refreshed', ttl, now) " +
"return allowed";
public boolean tryAcquire(String userId, int requests) {
// 用户级别限流:每个用户每秒最多2次
String key = "ai:limiter:" + userId;
long now = System.currentTimeMillis() / 1000;
Long result = redisTemplate.execute(
new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
Collections.singletonList(key),
"2", // 每秒2个令牌
"10", // 桶容量10(允许突发5秒内积压)
String.valueOf(now),
String.valueOf(requests)
);
return result != null && result == 1;
}
}
使用的时候就很简单:
java
@PostMapping("/generate")
public ResponseEntity generate(@RequestBody String prompt,
@RequestAttribute("userId") String userId) {
if (!aiLimiter.tryAcquire(userId, 1)) {
// 429是HTTP标准限流状态码
return ResponseEntity.status(429)
.body("{\"error\":\"操作太频繁,请稍后再试(狗头)\"}");
}
// 调用AI模型...
return ResponseEntity.ok(aiService.callModel(prompt));
}
这套方案支持用户级限流(按userId区分),也支持全局限流(把key换成固定值)。建议两者结合:单用户每秒2次,全局限流每秒100次,防止个别用户把整体资源打满。
第三层:高并发架构------别一压就垮
异步+削峰:消息队列保命
前面说的限流是"拦",但拦太狠了用户体验差。更好的做法是削峰填谷:把请求先丢进消息队列,慢慢消费。
想象一下春运期间的火车站。限流就是把站门锁了不让进(简单粗暴);消息队列就是把人放进候车厅排队,按车次慢慢检票(优雅)。
2026年的技术栈推荐用RocketMQ 5.x或Pulsar,比Kafka更适合这种需要严格消息顺序和延迟队列的场景。不过为了演示简单,这里用Redis的Stream结构(轻量级消息队列):
java
@Service
public class AIAsyncService {
@Autowired
private StringRedisTemplate redisTemplate;
// 投递任务到队列
public String submitTask(String userId, String prompt) {
String taskId = UUID.randomUUID().toString();
Map task = new HashMap<>();
task.put("userId", userId);
task.put("prompt", prompt);
task.put("status", "pending");
task.put("submitTime", String.valueOf(System.currentTimeMillis()));
// XADD往Redis Stream里塞任务
redisTemplate.opsForStream().add("ai:tasks", task);
return taskId;
}
// 消费者端(可用@Scheduled或Spring的@EventListener定时拉取)
@Scheduled(fixedDelay = 100)
public void consumeTasks() {
List> records =
redisTemplate.opsForStream().read(
StreamOffset.fromStart("ai:tasks")
);
for (MapRecord record : records) {
Map value = record.getValue();
try {
processTask(value);
// ACK确认,防止重复消费
redisTemplate.opsForStream().acknowledge("ai:tasks", record.getId());
} catch (Exception e) {
// 失败重试或进死信队列
log.error("任务处理失败: {}", value, e);
}
}
}
private void processTask(Map task) {
String prompt = (String) task.get("prompt");
// 调用AI模型...
String result = aiClient.syncCall(prompt);
// 存储结果,供用户轮询查询
redisTemplate.opsForValue().set("ai:result:" + task.get("id"), result, 30, TimeUnit.MINUTES);
}
}
前端配合轮询或WebSocket推送结果。这样即使瞬间涌入一万个请求,你的AI模型也只会按恒定速率(比如每秒5个)处理,系统稳如老狗。
熔断降级:AI挂了也别拖死系统
AI服务有个特点:不稳定。可能OpenAI那边抽风了,或者国产模型机房断网了。这时候如果你还死等,线程池很快就被占满,整个服务雪崩。
得用熔断器。Resilience4j是目前Spring Cloud生态的首选(Hystrix已经停止维护多年了,别再用了)。
java
@Service
public class AICallerService {
private final CircuitBreaker circuitBreaker;
private final RateLimiter rateLimiter;
public AICallerService() {
// 熔断配置:5秒内失败率超过50%就打开熔断
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(10000))
.slidingWindowSize(5)
.build();
this.circuitBreaker = CircuitBreaker.of("aiModel", cbConfig);
// 限流配置:每秒只允许10次调用
RateLimiterConfig rlConfig = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(10)
.timeoutDuration(Duration.ofMillis(100))
.build();
this.rateLimiter = RateLimiter.of("aiRate", rlConfig);
}
public String callWithProtection(String prompt) {
// 装饰器模式:先限流,再熔断,最后执行
Supplier decorated = Decorators.ofSupplier(() -> doRealCall(prompt))
.withCircuitBreaker(circuitBreaker)
.withRateLimiter(rateLimiter)
.withFallback(e -> "AI服务繁忙,请稍后重试(或试试我们的备用模型)")
.decorate();
return Try.ofSupplier(decorated).get();
}
private String doRealCall(String prompt) {
// 真实调用AI SDK...
return openAiClient.chatCompletion(prompt);
}
}
这里有个小技巧:降级策略。当主模型(比如GPT-4)熔断了,可以自动切换到备用模型(比如轻量级的qwen-turbo或本地部署的小模型),虽然回答质量差点,但至少服务可用。
监控与告警:眼见为实
防护做了,但得知道啥时候被攻击了。Prometheus + Grafana是标配,但针对AI接口,有几个特定指标必须监控:
java
@Component
public class AIMetrics {
// 用Micrometer埋点
private final MeterRegistry registry;
public void recordCall(String model, long latencyMs, boolean success) {
// 按模型维度统计QPS和延迟
registry.counter("ai.calls", "model", model, "status", success ? "success" : "fail").increment();
registry.timer("ai.latency", "model", model).record(latencyMs, TimeUnit.MILLISECONDS);
// 成本监控(关键!)
double cost = calculateCost(model, promptTokens, completionTokens);
registry.gauge("ai.cost", Tags.of("model", model), cost);
}
// 异常检测:如果某个IP的请求频次突然超过平时10倍,立即告警
public void checkAnomaly(String ip, int currentQps) {
String key = "ai:qps:baseline:" + ip;
Integer baseline = redisTemplate.opsForValue().get(key);
if (baseline != null && currentQps > baseline * 10) {
alertService.sendAlert("IP " + ip + " 可能存在异常调用,当前QPS: " + currentQps);
}
// 滑动窗口更新基线
redisTemplate.opsForValue().set(key, currentQps, 1, TimeUnit.HOURS);
}
}
告警阈值建议设两档:
- Warning:单用户QPS超过5,或单IP每分钟超过50次
- Critical:全局QPS突增300%,或平均延迟超过10秒(可能被DDoS)
总结:安全是门"成本艺术"
说实话,做安全防护就是和成本博弈。鉴权增加了计算开销(验签要CPU),限流需要Redis内存,异步架构增加了系统复杂度。但相比被黑产刷爆账单的损失,这些投入都是毛毛雨。
checklist 回顾一下:
- 鉴权:别裸奔,至少用上JWT+时间戳防重放
- 限流:令牌桶算法,用户级+全局双重限制
- 高并发:消息队列削峰,熔断器兜底,别信任AI服务的稳定性
- 监控:成本指标必须看,异常IP实时封禁
这套方案已经在生产环境扛住了多次小规模攻击(包括某次被爬虫误伤,峰值打到2000 QPS,系统稳住了,只有少部分用户排队等了半分钟)。代码都在上面了,根据自己业务改改就能用。
最后说句掏心窝的:315曝光的只是冰山一角。AI黑产的技术迭代速度比我们想象得快得多,今天写的防护逻辑,半年后可能就有新的绕过手法。保持警惕,多翻日志,没事看看账单有没有异常波动,这比任何技术方案都管用。
毕竟,没有绝对安全的系统,只有相对勤奋的运维。(手动狗头)