第 16 篇:AI 系统过不了等保?企业上线必做!敏感词 + 权限 + 审计日志全套方案(Java+AI 落地实战系列 | 直接上生产 | 杜绝翻车)

本文是《Java+AI 落地实战 从入门到生产级》系列第 16 篇
往期回顾:

  • 第1篇:Java程序员学AI/转AI全指南,从CURD到AI应用工程师(零算法,4周落地)

  • 第2篇:Java 后端转 AI,这条完整链路 90% 的人没搞懂!AI 怎么自动干活、Java转AI链路:LLM、RAG、FunctionCall、ToolCall、Skills、Agent、MCP

  • 第3篇:别再焦虑了!Java开发做AI,根本不用学Python

  • 第4篇:10分钟跑通!SpringBoot对接通义千问,实现AI对话功能

  • 第5篇:SpringBoot实现流式输出,和ChatGPT体验一模一样

  • 第6篇:对话记忆怎么实现?Java版多轮上下文对话完整方案

  • 第7篇:从零搭建本地RAG知识库,内网可用,零API费用

  • 第8篇:支持PDF/Word/Excel!Java实现多格式文档自动解析入库

  • 第9篇:别再用内存向量库了!Milvus向量数据库Java对接全指南

  • 第10篇:RAG检索准确率太低?5个优化技巧,准确率提升50%

  • 第11篇:多租户权限隔离!企业级知识库部门级权限管控方案

  • 第12篇:一键对接企业微信!把知识库装进企业微信,全员开箱即用

  • 第13篇:Java 对接通义千问大模型,性能优化全攻略

  • 第14篇:AI 只会聊天?教你千问点奶茶能力的底层技术

  • 第 15 篇:企业级 AI 接口怎么设计?限流、降级、熔断全套方案-涨薪30%的手段

上一篇我们基于 Sentinel 完成AI接口高可用架构,保障高峰期系统稳定、防止服务雪崩。很多小伙伴做完稳定性改造之后卡在最后一关:企业内部部署AI知识库,合规不达标,无法正式上线、过不了等保测评。

用户输入违规话术、AI输出敏感内容、员工越权查看其他部门文档、所有交互没有操作记录,任意一个问题都会成为上线拦路虎。今天落地一套端到端AI系统合规防护体系:输入输出双重敏感词校验、三级细粒度权限隔离、全链路操作审计日志,整套代码无缝接入我们前面搭建的RAG知识库项目,满足等保2.0标准,拿来就能用于企业项目验收。

先看合规改造前后的核心差异:

图片


一、AI知识库落地,等保测评4项硬性合规要求

企业内部AI系统、RAG知识库,等保评审重点核查4大维度,也是我们整套方案的实现目标:

下面沿着请求流转链路逐层实现,形成完整合规闭环。

图片


二、核心合规 1:输入输出双重敏感词过滤

内容安全第一道防线,遵循「违规内容进不来、敏感信息出不去」原则:不仅拦截用户输入,AI返回结果同样需要过滤。

技术选型:DFA确定有穷自动机

不推荐简单 contains 字符串匹配。DFA字典树算法性能极强,支持上万条敏感词库毫秒级匹配,不会拖慢问答接口响应,适配我们之前搭建的高并发AI问答服务。

@Component
public class SensitiveWordFilter {
    private final Map<String, Object> dfaMap = new HashMap<>();
    /**
     * 项目启动初始化敏感词字典树
     */
    @PostConstruct
    public void init() {
        // 生产环境:从数据库/配置文件动态加载敏感词,支持后台管理刷新词库
        List<String> sensitiveWords = SensitiveWordHolder.getAllSensitiveWords();
        for (String word : sensitiveWords) {
            addWord(word);
        }
    }
    /**
     * 构建DFA字典树
     */
    private void addWord(String word) {
        Map<String, Object> currentMap = dfaMap;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            Object nextMap = currentMap.get(String.valueOf(c));
            if (nextMap == null) {
                Map<String, Object> newMap = new HashMap<>();
                newMap.put("isEnd", false);
                currentMap.put(String.valueOf(c), newMap);
                currentMap = newMap;
            } else {
                currentMap = (Map<String, Object>) nextMap;
            }
            if (i == word.length() - 1) {
                currentMap.put("isEnd", true);
            }
        }
    }
    /**
     * 检测文本,返回命中全部敏感词
     */
    public List<String> checkSensitiveWord(String text) {
        List<String> result = new ArrayList<>();
        for (int i = 0; i < text.length(); i++) {
            int len = matchWord(text, i);
            if (len > 0) {
                result.add(text.substring(i, i + len));
                i += len - 1;
            }
        }
        return result;
    }
    /**
     * 敏感字符替换为*
     */
    public String replaceSensitiveWord(String text) {
        List<String> words = checkSensitiveWord(text);
        for (String word : words) {
            String replace = "*".repeat(word.length());
            text = text.replace(word, replace);
        }
        return text;
    }
    private int matchWord(String text, int startIndex) {
        boolean isEnd = false;
        int matchLen = 0;
        Map<String, Object> currentMap = dfaMap;
        for (int i = startIndex; i < text.length(); i++) {
            String c = String.valueOf(text.charAt(i));
            currentMap = (Map<String, Object>) currentMap.get(c);
            if (currentMap == null) {
                break;
            }
            matchLen++;
            if (Boolean.TRUE.equals(currentMap.get("isEnd"))) {
                isEnd = true;
            }
        }
        return isEnd ? matchLen : 0;
    }
}

统一拦截实现:输入拦截器 + 输出统一响应处理

采用Spring组件全局拦截,和之前Sentinel流量防护、全局统一返回格式完美兼容。

@Component
public class SensitiveWordInterceptor implements HandlerInterceptor {
    @Autowired
    private SensitiveWordFilter sensitiveWordFilter;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 用户提问输入校验
        String question = request.getParameter("question");
        if (StringUtils.isNotBlank(question)) {
            List<String> words = sensitiveWordFilter.checkSensitiveWord(question);
            if (!words.isEmpty()) {
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(JSON.toJSONString(Result.fail("提问包含违规内容,请调整后重试")));
                return false;
            }
        }
        return true;
    }
}

输出敏感词过滤我们采用 ResponseBodyAdvice 全局统一拦截,所有控制器返回结果在推送到前端之前自动执行敏感词替换,做到输入、输出双向防护。下面还有更重要的代码段、由于文章篇幅限制给出实现思路,完整源码放入配套资料包。

实现思路:

1、实现 ResponseBodyAdvice,重写 beforeBodyWrite;

2、只拦截 AI 问答相关返回结果,避免全局无差别处理损耗性能;

3、识别统一返回体 Result,提取回答内容执行敏感词替换;

4、组装新结果返回前端。

⚠️ 流式 SSE 长连接注意:SSE SseEmitter 不属于普通 ResponseBody,无法被该切面拦截。流式输出场景,需要在业务发送每一段文本时主动调用敏感词过滤器

SSE是什么?怎么用?传说中的打字机,Chatjpt、豆包都在用

三、核心合规 2:三级细粒度权限管控

承接我们第11篇多租户隔离、第7篇RAG知识库内容,升级权限体系:角色权限 + 部门数据权限 + 工具调用权限,解决员工越权查看其他部门知识库文档痛点。

  • 功能权限:普通员工/管理员/超级管理员,控制后台功能访问;
  • 数据权限:检索知识库文档自动过滤所属部门,不同部门数据隔离(RAG核心需求);
  • 工具权限:限制角色可调用工具,普通员工禁止访问财务、人事敏感接口。

登录用户上下文工具

/**
 * 登录用户上下文工具
 * 全局获取当前登录用户信息
 */
@Component
public class SecurityContextHolder {
    private static final ThreadLocal<LoginUser> USER_THREAD_LOCAL = new ThreadLocal<>();
    /**
     * 设置当前登录用户
     */
    public static void setCurrentUser(LoginUser loginUser) {
        USER_THREAD_LOCAL.set(loginUser);
    }
    /**
     * 获取当前登录用户
     */
    public static LoginUser getCurrentUser() {
        return USER_THREAD_LOCAL.get();
    }
    /**
     * 清除上下文(防止内存泄漏,过滤器最后必须调用)
     */
    public static void clear() {
        USER_THREAD_LOCAL.remove();
    }
}

AOP切面统一权限校验,侵入极低

@Aspect
@Component
public class PermissionAspect {
    @Around("@annotation(requiresPermission)")
    public Object checkPermission(ProceedingJoinPoint point, RequiresPermission requiresPermission) throws Throwable {
        // 获取当前登录用户(和项目现有登录上下文保持一致)
        LoginUser user = SecurityContextHolder.getCurrentUser();

        // 校验角色功能权限
        if (!user.getRoles().contains(requiresPermission.role())) {
            throw new PermissionException("当前账号无此操作权限");
        }

        // 设置当前用户部门ID,后续Mybatis自动拼接数据权限SQL
        DataPermissionHelper.setCurrentDeptId(user.getDeptId());
        try {
            return point.proceed();
        } finally {
            DataPermissionHelper.clear();
        }
    }
}

数据权限自动过滤,可以直接复用RAG文档检索Mapper,向量查询时追加dept_id筛选,实现多租户知识库隔离。

具体内容可查看:第11篇:多租户权限隔离!企业级知识库部门级权限管控方案(Java+AI落地实战系列 | 开箱即用 | 满足企业数据安全合规)

四、核心合规 3:全链路操作审计日志

等保硬性要求:所有用户交互行为可追溯。用户提问、文档上传、向量库新增文档、工具调用、管理员配置修改全部记录。

审计日志数据库实体

@Data
@TableName("ai_operation_log")
public class OperationLog {
    private Long id;
    private String userId;      // 操作人ID
    private String userName;    // 操作人姓名
    private String deptId;      // 所属部门
    private String operationType; // 操作类型:提问、上传文档、调用工具
    private String content;     // 操作内容
    private String ip;          // 操作IP地址
    private LocalDateTime createTime; // 操作时间
    private Integer success;    // 操作是否成功 1成功 0失败
}

自定义注解代码

/**
 * 操作审计日志注解
 * 标记在Controller接口方法上,自动记录全链路操作日志
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
    /**
     * 操作类型:提问、上传文档、调用工具、知识库管理等
     */
    String type();
}

AOP自动记录审计日志,异步入库不阻塞主流程

@Aspect
@Component
public class OperationLogAspect {
    @Autowired
    private OperationLogMapper logMapper;
    @Around("@annotation(operationLog)")
    public Object recordLog(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
        LoginUser user = SecurityContextHolder.getCurrentUser();
        OperationLog log = new OperationLog();
        log.setUserId(user.getUserId());
        log.setUserName(user.getUserName());
        log.setDeptId(user.getDeptId());
        log.setOperationType(operationLog.type());
        log.setContent(JSON.toJSONString(joinPoint.getArgs()));
        log.setIp(ServletUtil.getClientIP());
        log.setCreateTime(LocalDateTime.now());
        // 默认标记成功
        log.setSuccess(1);
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            // 发生异常,标记操作失败
            log.setSuccess(0);
            throw e;
        } finally {
            // 无论成功失败,异步写入日志
            CompletableFuture.runAsync(() -> logMapper.insert(log));
        }
        return result;
    }
}

图片

五、新手落地必踩5个致命合规坑

坑1:只校验用户输入,忽略AI输出审核

很多开发只拦截用户提问,忽略大模型生成内容。AI极易被提示词诱导输出敏感信息,带来重大合规风险。

✅ 方案:输入、输出双向校验,AI回答返回前端之前强制经过敏感词过滤器。

坑2:权限仅前端控制,后端不做校验

前端隐藏按钮不等于权限控制,用户抓包直接调用接口,轻松实现越权访问其他部门知识库。

✅ 方案:权限校验强制放在后端AOP切面,前端仅做页面展示优化。

坑3:敏感词库杂乱,误杀率过高

直接网上下载通用敏感词库,正常业务词汇被拦截,严重影响员工使用体验。

✅ 方案:按企业业务自定义词库,分级管控:高危词汇直接拦截、普通违规词汇内容替换;持续迭代优化降低误判。

坑4:审计日志原样存储手机号、身份证等敏感信息

日志明文存储隐私数据,日志库一旦泄露,直接违反《个人信息保护法》。

✅ 方案:日志入库前统一脱敏,手机号、证件号中间字符使用*遮挡。

坑5:同步写入审计日志,拖慢问答接口

问答接口同步写入数据库,数据库压力大时直接造成接口超时。

✅ 方案:采用异步线程写入日志;高并发场景可引入消息队列削峰。
关注公众号 Java-AI 工程师(有好礼),

_cgi-bin_mmwebwx-bin_webwxgetmsgimg__&MsgID=4502347792639468853&skey=@crypt_f25bfbc0_aa4cdb49bb11a32e7d0440680b06084e&mmweb_appid=wx_webfilehelper

后台回复 系列源码 即可免费领取全部的源码和资料。每更新一篇,我都会往资料包里新增对应源码,跟着系列就能从零搭出完整的企业级 AI 系统。并且每周都有面试题刷哦

✅ 本篇学习完成

恭喜你,勇士,咱们Boss见,此时您已经能完整的手搓出来一个合规、高可用、高性能、高并发的AI系统了。

本系列共 21 篇,目前你已经完成了16篇的学习,21篇全部学习完成,包您涨薪。放松下,再回来看下面的优化建议和总结。劳逸结合才能把知识消化得更扎实。

posted @ 2026-08-11 09:20  公众号|Java-AI工程师  阅读(189)  评论(0)    收藏  举报