SpringBoot + 百度内容安全实战:自定义注解 + AOP 实现统一内容审核(支持文本 / 图片 / 视频 + 白名单 + 动态开关)

🧩 一、为什么要做内容审核

在实际业务(如用户发帖、评论、任务描述、AI生成内容等)中,往往需要防止以下问题:

  • 🧨 用户发布涉政、色情、辱骂等敏感内容;
  • 🖼️ 上传违规图片或视频;
  • 🤖 机器人刷垃圾广告、推广链接;
  • ⚙️ 需要为不同场景设置灵活的开关与白名单。

这篇文章带你从 架构设计代码实现 ,一步步构建出企业级的 内容审核中间层


📘 二、核心功能概览

支持类型 :文本、图片、视频

实现方式 :自定义注解 + AOP 自动拦截

集成能力 :百度内容安全 API(AipContentCensor

动态配置 :从配置中心获取审核开关与白名单(infraConfigApi

精准提示 :自动定位哪个字段违规,展示自定义提示信息

业务无侵入:通过注解实现自动校验


🧱 三、完整架构设计图

plain 复制代码
Controller → Service → (AOP 拦截)
                   ↓
             MyValidAspect
                   ↓
             BaiduCheck工具类
                   ↓
       百度内容审核接口(文本/图片/视频)

⚙️ 四、核心实现代码(附详细注释)

1️⃣ 自定义注解:@ContentCheck

java 复制代码
@Target({ElementType.TYPE_USE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ContentCheck {

    /** 内容类型枚举 */
    enum ContentType {
    TEXT,  // 文本内容
    IMAGE  // 图片内容
}

/** 检查失败时的提示信息 */
String message() default "内容检查不合规";

/** 默认是文本类型 */
ContentType value() default ContentType.TEXT;
}

🔹 说明:

  • 可标记在字段或方法上;
  • 每个字段都可以定义自己的 message() 提示内容;
  • 区分文本与图片类型。

2️⃣ 百度内容安全工具类:BaiduCheck

java 复制代码
@Slf4j
public class BaiduCheck {

    // 百度控制台申请的 API_KEY、SECRET_KEY
    public static final String API_KEY = "xxxxxxxxxxxxx";
    public static final String SECRET_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxx";

    // 初始化内容审核客户端
    static AipContentCensor client = new AipContentCensor("222222222", API_KEY, SECRET_KEY);

    /**
     * 文本审核(简单判断:true 为安全)
     */
    public static boolean baiduTextCheck(String text) throws IOException {
        if (StringUtils.isEmpty(text)) {
            return true;
        }
        JSONObject jsonObject = client.textCensorUserDefined(text);
        return jsonObject.getInt("conclusionType") <= 1;
    }

    /**
     * 文本审核(返回第一个命中的敏感词;null 表示安全)
     */
    public static String baiduTextCheck01(String text) {
        if (StringUtils.isEmpty(text)) {
            return null;
        }
        JSONObject jsonOriginObject = client.textCensorUserDefined(text);
        JsonNode jsonObject = JsonUtils.parseTree(jsonOriginObject.toString());
        log.info("文本:{}, 审核结果:{}", text, jsonObject.toString());

        if (jsonObject.has("conclusionType") && jsonObject.get("conclusionType").asInt() > 1) {
            try {
                // ⚙️ 白名单优先:type=14 的内容被百度标记为"合规"
                if (jsonObject.get("data") != null) {
                    for (JsonNode next : jsonObject.get("data")) {
                        if (next.get("type").asInt() == 14) {
                            return null;
                        }
                    }
                }
                // 返回第一个命中的词
                return jsonObject.get("data").get(0).get("hits").get(0).get("words").get(0).asText();
            } catch (Exception e) {
                return "";
            }
        }
        return null;
    }

    /**
     * 图片检测(任一违规则返回 false)
     */
    public static boolean baiduImageCheck(String... urls) {
        try {
            for (String url : urls) {
                JSONObject jsonObject = client.imageCensorUserDefined(url, EImgType.URL, null);
                if (jsonObject.getInt("conclusionType") > 1) {
                    return false;
                }
            }
        } catch (Exception e) {
            log.error("图片检测异常", e);
        }
        return true;
    }

    /**
     * 视频检测(违规即返回 false)
     */
    public static boolean baiduSortVideoCheck(String name, String videoUrl, String id) {
        JSONObject jsonObject = client.videoCensorUserDefined(name, videoUrl, id, null);
        return jsonObject.getInt("conclusionType") <= 1;
    }

    /**
     * 生成 Access Token,用于 NLP 检查
     */
    static String getAccessToken() throws IOException {
        OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder().build();
        RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"),
                                              "grant_type=client_credentials&client_id=" + API_KEY + "&client_secret=" + SECRET_KEY);
        Request request = new Request.Builder()
        .url("https://aip.baidubce.com/oauth/2.0/token")
                .method("POST", body)
                .build();
        Response response = HTTP_CLIENT.newCall(request).execute();
        return new JSONObject(response.body().string()).getString("access_token");
    }
}

3️⃣ 核心切面:MyValidAspect

🧭 业务集成点标注说明

  • infraConfigApi: 从配置中心读取审核开关、白名单
    • InfraConfigEnum.ConfigKeyEnum.TASK_SWITCH_CONFIG:动态配置字典的key
    • infraConfigApi.getJsonObjectByConfigKey(...): 动态控制开关
      • 拿到的是一个json 从json里面取key为textCheck的属性就是开关
      • 从json里取key为textCheckWhiteList 就是用户白名单数组
  • CONTENT_CHECK_ERROR: 自定义业务错误码
    • 可以自己定义
  • SecurityFrameworkUtils.getLoginUserId(): 获取当前用户ID
    • 通过读者自己使用的方式获取
  • ServiceExceptionUtil.exception(): 统一异常抛出工具
    • 直接使用 new RuntimeException()或者自己定义一个工具类
java 复制代码
@Aspect
@Slf4j
public class MyValidAspect {

    @Resource
    private InfraConfigApi infraConfigApi; // ⚙️ 业务配置中心,用于获取开关与白名单

    /**
     * 拦截所有标注了 @ContentCheck 的方法
     */
    @Before("@annotation(contentCheck)")
    public void doBefore(JoinPoint joinPoint, ContentCheck contentCheck) {
        StringBuffer sbText = new StringBuffer();
        List<String> imageLst = new ArrayList<>();

        Arrays.stream(joinPoint.getArgs()).forEach(e -> {
            Class<?> clazz = e.getClass();
            HashMap<ContentCheck, String> fieldValueHashMap = new HashMap<>();

            // 反射扫描参数中的带注解字段
            Arrays.stream(ReflectUtil.getFields(clazz))
            .filter(field -> field.isAnnotationPresent(ContentCheck.class))
            .forEach(field -> {
                ContentCheck annotation = field.getAnnotation(ContentCheck.class);
                String fieldValue = (String) ReflectUtil.getFieldValue(e, field);
                if (StrUtil.isEmpty(fieldValue)) {
                    return;
                }
                // 根据类型分类收集
                if (annotation.value() == ContentCheck.ContentType.TEXT) {
                    sbText.append(fieldValue);
                    fieldValueHashMap.put(annotation, fieldValue);
                } else if (annotation.value() == ContentCheck.ContentType.IMAGE) {
                    imageLst.addAll(Arrays.asList(fieldValue.split(",")));
                }
            });

            // === 📘 文本检测逻辑 ===
            String sensitiveStr = null;
            if (sbText.length() > 0) {
                sensitiveStr = textCheck01(sbText.toString());
            }

            // 审核通过则直接返回
            if (sensitiveStr == null) {
                textSuccessResult = true;
                return;
            }
            //  图片的校验
            // if (textSuccessResult && !imageLst.isEmpty() && BaiduCheck.baiduImageCheck(imageLst.toArray(new String[0]))) {
            //     return true;
            // }

            // 命中违规内容:匹配字段并取对应 message 提示
            String msg = contentCheck.message();
            for (Map.Entry<ContentCheck, String> next : fieldValueHashMap.entrySet()) {
                if (next.getValue().contains(sensitiveStr)) {
                    msg = next.getKey().message();
                    break;
                }
            }

            // 抛出统一业务异常
            throw ServiceExceptionUtil.exception(CONTENT_CHECK_ERROR, msg);
        });
    }

    /**
     * 普通文本检测(受配置中心控制)
     */
    public boolean textCheck(String content) {
        JSONObject json = infraConfigApi.getJsonObjectByConfigKey(InfraConfigEnum.ConfigKeyEnum.TASK_SWITCH_CONFIG);
        if (json.getBool("textCheck").equals(false)) {
            return true; // 配置关闭时直接跳过
        }
        try {
            return BaiduCheck.baiduTextCheck(content);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 文本检测(带白名单逻辑)
     */
    public String textCheck01(String content) {
        JSONObject json = infraConfigApi.getJsonObjectByConfigKey(InfraConfigEnum.ConfigKeyEnum.TASK_SWITCH_CONFIG);
        if (json.getBool("textCheck").equals(false)) {
            return null;
        }
        try {
            Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); // ⚙️ 当前登录用户ID(业务相关)
            JSONArray textCheckWhiteList = json.getJSONArray("textCheckWhiteList");
            if (loginUserId != null && CollectionUtil.isNotEmpty(textCheckWhiteList)) {
                List<Long> list = textCheckWhiteList.toList(Long.class);
                if (list.contains(loginUserId)) {
                    return null; // 白名单用户跳过审核
                }
            }
        } catch (Exception e) {
            log.error("内容检查白名单error: {}", e.getMessage());
        }
        return BaiduCheck.baiduTextCheck01(content);
    }
}

🧩 五、使用示例


✅ 六、运行效果


🧠 七、总结

功能模块 说明
@ContentCheck 声明式注解,定义检测规则
MyValidAspect AOP 拦截执行统一检测
BaiduCheck 封装百度AI审核接口
infraConfigApi 动态控制开关 & 白名单
SecurityFrameworkUtils 用户上下文信息获取

优势总结:

  • 📦 无侵入接入:业务层零改动;
  • 🧩 配置化管理:审核可动态启停;
  • 🛡️ 白名单机制:灵活控制;
  • ⚙️ 可扩展:后续可接入阿里云、腾讯云多引擎审核;
  • 🧠 智能提示:精准定位违规字段。
相关推荐
方才coding3 小时前
25年11月系分架构论文:论安全架构设计
安全·架构·安全架构
孫治AllenSun3 小时前
【系统安全】DDoS攻击
安全·系统安全·ddos
yzq-38413 小时前
Websocket两台服务器之间的通信
spring boot·websocket·网络协议
GIS数据转换器4 小时前
基于GIS的智慧畜牧数据可视化监控平台
人工智能·安全·信息可视化·无人机·智慧城市·制造
我叫汪枫4 小时前
《HTTP 安全与性能优化全攻略》
安全·http·性能优化
摇滚侠4 小时前
Spring Boot3零基础教程,SpringSecurity 测试,笔记81
spring boot·笔记·后端
小白黑科技测评4 小时前
2025 年视频去水印工具实测:擦擦视频双版本解析一键去字幕与多格式兼容能力
java·人工智能·音视频·智能电视·1024程序员节
雷达学弱狗5 小时前
播放bilibili视频,视频正常加载,但是无法播放一直转圈。机型拯救者R9000P 2023
音视频
Swift社区5 小时前
Foundation Model 在 Swift 中的类型安全生成实践
开发语言·安全·swift