构建安全防线:AI对话应用中的分布式限流与Prompt安全审查实战

在人工智能应用蓬勃发展的今天,AI对话接口已成为众多产品的核心功能。然而,随着用户量的激增,接口安全与稳定性面临着严峻挑战。恶意攻击、资源滥用以及Prompt注入等安全问题,不仅可能导致服务瘫痪、成本飙升,更可能引发数据泄露等严重后果。本文将深入探讨如何为你的AI对话应用构建一套坚实的安全防线,重点聚焦于分布式限流Prompt安全审查(Guardrails)两大核心策略,并结合Spring Boot、Redisson与LangChain4j等主流技术栈,提供一套可落地的实战方案。

一、 分布式限流:抵御流量洪峰的第一道闸门

AI对话生成,尤其是基于深度学习大模型的接口,通常是计算密集型服务,调用成本高昂。无限制的访问极易被恶意爬虫或脚本攻击,导致服务器资源耗尽。传统的单机限流在微服务或集群环境下力不从心,因此我们需要引入分布式限流。Redisson作为一款优秀的Redis Java客户端,其内置的RRateLimiter为我们提供了分布式、高可用的限流解决方案。

首先,我们需要在项目中引入Redisson的依赖。这一步是构建所有分布式功能的基础。



    org.springframework.session
    spring-session-data-redis



  org.redisson
  redisson
  3.50.0

接下来,在应用的配置文件中,我们需要正确配置Redis连接信息,确保Redisson客户端能够成功连接至Redis服务器。

# redis
spring:
  data:
    redis:
      host: localhost
      port: 6379
      ttl: 3600
      database: 0
      password:

随后,我们编写Redisson的配置类,将其注入Spring容器,以便在项目中任何地方都能方便地使用。

@Configuration
public class RedissonConfig {
    @Value("${spring.data.redis.host}")
    private String redisHost;
    @Value("${spring.data.redis.port}")
    private int redisPort;
    @Value("${spring.data.redis.password}")
    private String redisPassword;
    @Value("${spring.data.redis.database}")
    private int redisDataBase;
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        String address = "redis://" + redisHost + ":" + redisPort;
        SingleServerConfig singleServerConfig = config.useSingleServer()
        .setAddress(address)
        .setDatabase(redisDataBase)
        .setConnectionMinimumIdleSize(1)
        .setConnectionPoolSize(10)
        .setConnectTimeout(5000)
        .setIdleConnectionTimeout(30000)
        .setTimeout(3000)
        .setRetryAttempts(3)
        .setRetryDelay(new ConstantDelay(Duration.ofMillis(1500)));
        if (redisPassword != null && !redisPassword.isEmpty()) {
            singleServerConfig.setPassword(redisPassword);
        }
        return Redisson.create(config);
    }
}

为了提升代码的可维护性和灵活性,我们可以定义一个限流规则的枚举类。这样,不同接口的限流策略(如每秒次数、每分钟次数)就可以集中管理,一目了然。

public enum RateLimitType {
    /**
     * 接口级别限流
     */
    API,
    /**
     * 用户级别限流
     */
    USER,
    /**
     * IP级别限流
     */
    IP
}

限流逻辑本质上是一种横切关注点(Cross-cutting Concern),非常适合使用面向切面编程(AOP)来实现。我们创建一个自定义注解,将其标注在需要限流的方法上,从而实现声明式的限流配置,代码侵入性极低。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    /**
     * 限流key前缀
     */
    String key() default "";
    /**
     * 每个时间窗口的请求速率(允许的请求数)
     */
    int rate() default 10;
    /**
     * 时间窗口大小(单位:秒)
     */
    int rateInterval() default 1;
    /**
     * 限流类型
     */
    RateLimitType limitType() default RateLimitType.USER;
    /**
     * 限流提示信息
     */
    String message() default "请求过于频繁,请稍后再试";
}

注解需要切面来赋予其灵魂。下面我们实现这个限流切面,其核心逻辑是:在目标方法执行前,通过Redisson的RRateLimiter尝试获取令牌。若获取成功则放行;若失败(即触发限流),则抛出特定的限流异常。

@Aspect
@Component
@Slf4j
public class RateLimitAspect {
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private UserService userService;
    @Before("@annotation(rateLimit)")
    public void doBefore(JoinPoint point, RateLimit rateLimit){
        String key = generateRateLimitKey(point, rateLimit);
        // 使用Redisson的分布式限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        rateLimiter.expire(Duration.ofHours(1));
        // 设置限流器参数,每个时间窗口允许的请求数和时间窗口
        rateLimiter.trySetRate(RateType.OVERALL, rateLimit.rate(), rateLimit.rateInterval(), RateIntervalUnit.SECONDS);
        // 尝试获取令牌,如果获取失败则限流
        if (!rateLimiter.tryAcquire(1)) {
            throw new BusinessException(ErrorCode.TOO_MANY_REQUEST_ERROR, rateLimit.message());
        }
    }
    private String generateRateLimitKey(JoinPoint point, RateLimit rateLimit) {
        StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append("rate_limit:");
        // 添加自定义前缀
        if (!rateLimit.key().isEmpty()) {
            keyBuilder.append(rateLimit.key()).append(":");
        }
        // 根据限流类型生成不同的key
        switch (rateLimit.limitType()) {
            case API:
                // 接口级别:方法名
                MethodSignature signature = (MethodSignature) point.getSignature();
                Method method = signature.getMethod();
                keyBuilder.append("api:").append(method.getDeclaringClass().getSimpleName())
                .append(".").append(method.getName());
                break;
            case USER:
                // 用户级别:用户ID
                try {
                    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                    if (attributes != null) {
                        HttpServletRequest request = attributes.getRequest();
                        User loginUser = userService.getLoginUser(request);
                        keyBuilder.append("user:").append(loginUser.getId());
                    } else {
                        // 无法获取请求上下文,使用IP限流
                        keyBuilder.append("ip:").append(getClientIP());
                    }
                } catch (BusinessException e) {
                    // 未登录用户使用IP限流
                    keyBuilder.append("ip:").append(getClientIP());
                }
                break;
            case IP:
                // IP级别:客户端IP
                keyBuilder.append("ip:").append(getClientIP());
                break;
            default:
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的限流类型");
        }
        return keyBuilder.toString();
    }
    private String getClientIP() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return "unknown";
        }
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 处理多级代理的情况
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip != null ? ip : "unknown";
    }
}

现在,我们可以在核心的AI对话生成接口上,轻松地应用这个注解。例如,限制每个用户每分钟只能调用5次。

/**
 * 应用聊天生成代码(流式 SSE)
 *
 * @param appId   应用 ID
 * @param message 用户消息
 * @param request 请求对象
 * @return 生成结果流
 */
@GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@RateLimit(rate = 5, rateInterval = 60, limitType = RateLimitType.USER, message = "AI 对话请求过于频繁,请稍后再试")
public Flux> chatToGenCode(@RequestParam Long appId,
                                                   @RequestParam String message,
                                                   HttpServletRequest request) {
[AFFILIATE_SLOT_1]

二、 SSE流式响应的限流异常处理优化

许多现代AI对话应用为了提升用户体验,会采用Server-Sent Events(SSE)技术进行流式响应。这带来了一个新的挑战:当限流切面抛出异常时,该异常在SSE流建立之前就被全局异常处理器拦截了,导致前端无法以流的形式接收到错误信息,用户体验断裂。

SSE是一种允许服务器主动向客户端推送数据的协议。其消息格式有明确规范:

event: 事件类型
data: 数据内容

为了解决上述问题,我们需要优化全局异常处理器,使其能够智能识别当前请求是否为SSE请求,并按照SSE格式返回错误事件。核心思路是检查请求的Accept头是否包含text/event-stream

@Hidden
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public BaseResponse businessExceptionHandler(BusinessException e) {
        log.error("BusinessException", e);
        // 尝试处理 SSE 请求
        if (handleSseError(e.getCode(), e.getMessage())) {
            return null;
        }
        // 对于普通请求,返回标准 JSON 响应
        return ResultUtils.error(e.getCode(), e.getMessage());
    }
    @ExceptionHandler(RuntimeException.class)
    public BaseResponse runtimeExceptionHandler(RuntimeException e) {
        log.error("RuntimeException", e);
        // 尝试处理 SSE 请求
        if (handleSseError(ErrorCode.SYSTEM_ERROR.getCode(), "系统错误")) {
            return null;
        }
        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
    }
    /**
     * 处理SSE请求的错误响应
     *
     * @param errorCode 错误码
     * @param errorMessage 错误信息
     * @return true表示是SSE请求并已处理,false表示不是SSE请求
     */
    private boolean handleSseError(int errorCode, String errorMessage) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return false;
        }
        HttpServletRequest request = attributes.getRequest();
        HttpServletResponse response = attributes.getResponse();
        // 判断是否是SSE请求(通过Accept头或URL路径)
        String accept = request.getHeader("Accept");
        String uri = request.getRequestURI();
        if ((accept != null && accept.contains("text/event-stream")) ||
            uri.contains("/chat/gen/code")) {
            try {
                // 设置SSE响应头
                response.setContentType("text/event-stream");
                response.setCharacterEncoding("UTF-8");
                response.setHeader("Cache-Control", "no-cache");
                response.setHeader("Connection", "keep-alive");
                // 构造错误消息的SSE格式
                Map errorData = Map.of(
                    "error", true,
                    "code", errorCode,
                    "message", errorMessage
                );
                String errorJson = JSONUtil.toJsonStr(errorData);
                // 发送业务错误事件(避免与标准error事件冲突)
                String sseData = "event: business-error\ndata: " + errorJson + "\n\n";
                response.getWriter().write(sseData);
                response.getWriter().flush();
                // 发送结束事件
                response.getWriter().write("event: done\ndata: {}\n\n");
                response.getWriter().flush();
                // 表示已处理SSE请求
                return true;
            } catch (IOException ioException) {
                log.error("Failed to write SSE error response", ioException);
                // 即使写入失败,也表示这是SSE请求
                return true;
            }
        }
        return false;
    }
}

相应地,前端也需要更新EventSource的事件监听逻辑,增加对自定义错误事件(如limit_error)的处理,以便在界面上友好地展示限流提示。

// 处理business-error事件(后端限流等错误)
eventSource.addEventListener('business-error', function (event: MessageEvent) {
  if (streamCompleted) return
  try {
    const errorData = JSON.parse(event.data)
    console.error('SSE业务错误事件:', errorData)
    // 显示具体的错误信息
    const errorMessage = errorData.message || '生成过程中出现错误'
    messages.value[aiMessageIndex].content = `❌ ${errorMessage}`
    messages.value[aiMessageIndex].loading = false
    message.error(errorMessage)
    streamCompleted = true
    isGenerating.value = false
    eventSource?.close()
  } catch (parseError) {
    console.error('解析错误事件失败:', parseError, '原始数据:', event.data)
    handleError(new Error('服务器返回错误'), aiMessageIndex)
  }
})

经过优化后,测试效果如下图所示。在一分钟内快速连续点击生成按钮超过5次后,前端清晰地收到了服务端通过SSE流推送的限流错误信息,体验流畅且符合预期。

三、 Prompt安全审查:构筑AI应用的“护轨”机制

流量保护是外功,内容安全则是内功。在自然语言处理(NLP)应用中,用户输入的Prompt可能包含恶意指令,试图诱导AI模型输出不当内容、泄露系统提示词(Prompt注入攻击)或执行未授权的操作。这就需要在将Prompt提交给神经网络模型之前,进行一道安全检查。

如下图所示,一个健壮的AI对话流程应在执行前加入安全审核环节。

这种安全机制在业界常被称为“护轨”(Guardrails)。顾名思义,它就像公路两旁的护栏,确保AI应用在安全的轨道内运行。护轨主要分为两类:

  • 输入护轨(Input Guardrails):在用户输入传递给AI模型之前进行检查和过滤。这是防范Prompt注入的第一道防线。
  • 输出护轨(Output Guardrails):在AI模型生成内容后进行检查和过滤。用于确保输出内容合法、合规、无害。

借助LangChain4j等成熟的AI应用框架,我们可以轻松集成护轨功能。下面实现一个基础的输入护轨示例,用于演示如何拒绝过长或包含明显敏感词的Prompt。请注意,生产环境可能需要集成更专业的敏感词检测服务或AI审核模型。

public class PromptSafetyInputGuardrail implements InputGuardrail {
    // 敏感词列表
    private static final List SENSITIVE_WORDS = Arrays.asList(
        "忽略之前的指令", "ignore previous instructions", "ignore above",
        "破解", "hack", "绕过", "bypass", "越狱", "jailbreak"
    );
    // 注入攻击模式
    private static final List INJECTION_PATTERNS = Arrays.asList(
        Pattern.compile("(?i)ignore\\s+(?:previous|above|all)\\s+(?:instructions?|commands?|prompts?)"),
        Pattern.compile("(?i)(?:forget|disregard)\\s+(?:everything|all)\\s+(?:above|before)"),
        Pattern.compile("(?i)(?:pretend|act|behave)\\s+(?:as|like)\\s+(?:if|you\\s+are)"),
        Pattern.compile("(?i)system\\s*:\\s*you\\s+are"),
        Pattern.compile("(?i)new\\s+(?:instructions?|commands?|prompts?)\\s*:")
    );
    @Override
    public InputGuardrailResult validate(UserMessage userMessage) {
        String input = userMessage.singleText();
        // 检查输入长度
        if (input.length() > 1000) {
            return fatal("输入内容过长,不要超过 1000 字");
        }
        // 检查是否为空
        if (input.trim().isEmpty()) {
            return fatal("输入内容不能为空");
        }
        // 检查敏感词
        String lowerInput = input.toLowerCase();
        for (String sensitiveWord : SENSITIVE_WORDS) {
            if (lowerInput.contains(sensitiveWord.toLowerCase())) {
                return fatal("输入包含不当内容,请修改后重试");
            }
        }
        // 检查注入攻击模式
        for (Pattern pattern : INJECTION_PATTERNS) {
            if (pattern.matcher(input).find()) {
                return fatal("检测到恶意输入,请求被拒绝");
            }
        }
        return success();
    }
}

实现护轨后,我们需要将其集成到AI服务的创建工厂中,确保所有对话请求都默认经过这道安全检查。

yield AiServices.builder(AiCodeGeneratorService.class)
        .streamingChatModel(reasoningStreamingChatModel)
        .chatMemoryProvider(memoryId -> chatMemory)
        .tools(toolManager.getAllTools())
        .inputGuardrails(new PromptSafetyInputGuardrail())  // 添加输入护轨
        .build();

如果你希望更精细地控制,也可以选择使用注解的方式,仅为特定方法添加护轨,提供更大的灵活性。

public interface Assistant {
    @InputGuardrails({ FirstInputGuardrail.class, SecondInputGuardrail.class })
    String chat(String question);
    String doSomethingElse(String question);
}
[AFFILIATE_SLOT_2]

四、 实践总结与进阶思考

通过上述实战,我们成功为AI对话应用搭建了双层安全防护:外层通过Redisson分布式限流抵御异常流量冲击,保护基础设施;内层通过LangChain4j护轨机制审查Prompt内容,保障AI交互的本质安全。测试效果清晰展示了这两道防线的有效性。

机器学习和AI应用高速发展的当下,安全是一个需要持续投入和演进的话题。除了本文介绍的方法,你还可以进一步探索:

  • 动态限流:根据系统负载(如CPU、内存使用率)动态调整限流阈值。
  • 多维度限流:结合用户ID、IP地址、API密钥等多维度进行更精细的流量控制。
  • 智能内容审核:接入专业的AI内容安全审核API,应对更复杂、更隐蔽的恶意输入。
  • 审计与溯源:记录所有被拦截的请求和Prompt,用于安全分析和模型迭代。

将安全思维嵌入AI应用开发的每一个环节,方能构建出既强大又可靠的智能系统,让技术真正造福于用户。

posted on 2026-03-21 12:05  blfbuaa  阅读(0)  评论(0)    收藏  举报