资讯动态

基于SpringBoot与Tika构建内容级DLP敏感信息检测服务

发布时间:2026/10/20 16:23:19 来源:尧图企业网站定制
做业务系统的人八成收到过安全部门发来的通知某个上传模块里发现了客户手机号请立即排查。我第一次接到这种通报时第一反应是收紧上传文件的扩展名白名单改成只能传图片和PDF结果没撑过两周第二封通报就来了——有人把docx后缀改成jpg传上来系统照样收货不误。从那以后我明白了一个道理要防的不是格式是内容本身。与其跟文件后缀较劲不如做一套能读内容、认敏感信息的服务。这个方案最终落地的形态就是基于SpringBoot和Tika搭建的数据泄露防护DLP服务。Tika负责把docx、pdf、xlsx、压缩包里的文本内容抽出来SpringBoot负责对外提供上传、检测、告警的接口再加上一层基于规则引擎的敏感信息匹配身份证号、手机号、银行卡号这类高敏数据一旦在文件内容里出现系统就能自动识别、拦截或脱敏。这篇文章把我实际搭建过程中踩过的坑、调整过的设计、最后验证过的代码完整记录下来适合正在做文件上传场景、想要补上内容安全检测能力的后端开发者也适合安全团队想快速做一个内部检测Demo的同事参考。1. 先想清楚这个系统到底要检测什么怎么检测1.1 DLP需求里的隐藏场景数据泄露防护这个词听起来偏安全基建但落到业务系统里其实就是几个非常具体的场景。最典型的工单系统的附件里经常塞简历、合同扫描件、身份证照片内容管理平台的上传入口每年要处理成百上千份文档其中夹杂客户通讯录甚至内部知识库里有人直接传了一份包含员工薪资和手机号的Excel。这些文件一旦被外部访问到或者被爬虫抓走就是实打实的泄露事件。我的第一版方案是拦截敏感文件的扩展名比如禁止上传xlsx、禁止上传docx结果业务部门强烈反对。改后缀上传这个操作成本几乎为零Windows里重命名一下就绕过了。这说明扩展名拦截只能防君子防不了所有情况必须做内容级检测。内容级检测的技术路线其实很清晰能读文件里的文本才能判断文本里有没有敏感信息。但问题马上就来了业务系统里的文件格式五花八门docx的内部结构是zippdf有对象流和压缩流xlsx本质是打包的XML老旧的WPS格式又是另一套东西。如果为每种格式单独写解析器光维护文档格式兼容性就够喝一壶了。我最初试着用POI解析docx和xlsx再单独用PDF库解析PDF代码写了不到两百行已经发现格式兼容性是个无底洞同样是docx不同WPS版本生成的文件结构还有细微差异有的解析器直接抛异常。1.2 为什么选Tika而不是自己写解析Tika是Apache下的内容检测工具包本质上是一大堆文档格式解析器Parser的集合它对外提供统一的文本抽取接口。我只需要把一个InputStream交给它它就能自动识别文件类型选对应的解析器把文本内容抠出来。这在DLP场景里价值很高因为我不需要关心上游业务系统传上来的是什么格式Tika会自己去识别和处理。Tika还有一个特性非常适合DLP场景就是它能根据文件内容的二进制特征识别真实格式而不是依赖扩展名。也就是说那个把docx改成jpg的绕过技巧在Tika面前无效。Tika检测文件头部魔数后能识别出这是一个Word文档而不是JPEG图片我们就能据此拒绝解析异常的文件。我对比过两条路。一条是引入POI、PDFBox等解析库挨个写逻辑优点是格式细节可控缺点是每种格式都有各自的API、异常处理、加密策略代码量成倍增长后续维护成本很高。另一条就是Tika的AutoDetectParser一个Parser接口兼容几十种格式。对DLP这类以文本匹配为核心的场景来说选择Tika明显更符合工程性价比。1.3 架构设计一条检测流水线三个关键决策整个检测服务最终做成了一条流水线从文件上传到处置完成依次经过文件大小校验、真实类型识别、文本抽取、敏感规则匹配、命中处置。这个流水线本身不复杂但设计时有三个关键决策直接决定了后续代码的结构。第一个决策是统一走“先抽文本、再匹配规则”的两步模式而不是对每种文件格式单独写规则。这个决策把问题域做了很好的分割文本抽取是Tika的事敏感匹配是我们自己的规则引擎。以后加新的敏感类型只需要在规则引擎里加规则不需要动解析层。第二个决策是敏感检测以正则为主、校验算法为辅纯正则匹配会把一串看起来像手机号的数字全部标红误报率高到业务没法用后面会细说。第三个决策是命中处置策略必须可配置同一个文件在面向公众的平台上可能是直接拦截在内部系统里可能只是告警加脱敏展示让策略跟着场景走而不是写死在代码里。2. 从零搭建项目配置与基础方案落地2.1 依赖引入与版本踩坑项目基于SpringBoot 2.7搭建JDK用的11。Tika方面引入两个依赖一个核心包一个解析器标准包。这是第一个坑很多旧教程还在让人引tika-parsers但在Tika 2.x里这个包已经拆分了直接引tika-parsers会因为找不到类而报错。正确做法是引标准解析器包tika-parsers-standard-package它涵盖了绝大多数常见格式的解析能力。dependency groupIdorg.apache.tika/groupId artifactIdtika-core/artifactId version2.9.0/version /dependency dependency groupIdorg.apache.tika/groupId artifactIdtika-parsers-standard-package/artifactId version2.9.0/version /dependency版本这块我建议锁定一个自己验证过的稳定版本而不是直接追最新。Tika的迭代速度不算慢新版本可能会调整某个解析器的行为哪怕只是升级了底层库也可能在慢慢蚕食你已有的兼容性覆盖。我一开始用了当时的最新版结果解析某老版本WPS生成的doc文件抛异常后来换回自己验证过的2.9.0才稳定从那以后都是锁版本不轻易动。同时引入SpringBoot的Web和Validation包这里不再赘述。如果项目里已经引入了其他文档解析库注意依赖冲突问题POI的版本和Tika内部管理的POI版本容易撞实际项目里我就遇到过一次NoSuchMethodError排查半天发现是POI版本被Tika传递依赖覆盖了最终方案是把业务代码里对POI的直接依赖去掉统一走Tika。2.2 文本抽取服务封装Tika的使用姿势有两种。一种是直接用门面类new Tika().parseToString(inputStream)方式非常简单几行代码就能拿到文本。但我不推荐在生产环境直接这么用因为门面类把底层细节全藏着没法限制解析输出的大小也没法拿到Metadata里的真实文件类型。恶意用户上传一个几百MB的超大文本一旦解析器把全部内容读进内存服务很快就会被拖垮。所以我的代码里选择了底层一点的AutoDetectParser组合WriteOutContentHandler的方式显式控制抽取出来的文本上限。下面是完整的文本抽取服务Service public class TextExtractionService { private static final int MAX_STRING_SIZE 50000; private final AutoDetectParser parser new AutoDetectParser(); public String extract(InputStream in, Metadata metadata) throws Exception { ParseContext context new ParseContext(); context.set(Parser.class, parser); WriteOutContentHandler handler new WriteOutContentHandler(MAX_STRING_SIZE); parser.parse(in, handler, metadata, context); return handler.toString(); } public String detectType(InputStream in, Metadata metadata) throws IOException { return parser.getDetector().detect(in, metadata).toString(); } }这段代码有两个需要特别说明的点。第一WriteOutContentHandler(MAX_STRING_SIZE)限制了文本抽取的最大字符数超过会抛WriteOutContentHandler.TooBigException这是个保护措施防止恶意超大文档打爆内存。50000字符对敏感信息检测来说已经足够正常简历、合同、邮件正文基本都覆盖到了。第二Metadata这个对象负责承载文件的元数据我们在调用前可以往里放文件名作为提示解析后可以从里面取CONTENT_TYPE拿真实格式。detectType方法是用来做真实类型识别的它调用了Tika的Detector基于文件内容特征判断类型而不是信任扩展名。这里要注意如果用同一个InputStream先调了detect再调parse流的指针已经到头了解析会拿不到内容。我在项目里踩过这个坑解决方案是把流先包成TikaInputStream它能支持重复读或者干脆从MultipartFile里重新获取一个新的流。2.3 敏感规则引擎设计规则引擎是整个检测链路的灵魂也是这里最容易让人翻车的地方。我第一版只用了简单正则很快就被误报折磨到怀疑人生。一轮测试下来一份正常的Excel表格里有几十个被标红的数字串业务同事看完直接拒绝验收。后来把规则引擎升级成三层匹配第一层是格式正则负责把候选字符串找出来。第二层是校验算法对特定类型的候选值做数学校验比如身份证的校验位、银行卡的Luhn算法这层能过滤掉大量形态相似但其实不合法的数字串。第三层是上下文规则结合文件里的上下文特征做二次判断比如手机号后面跟着“联系人”“电话”这类词时才计入高置信命中。我实际使用的核心正则和校验方法如下public final class SensitiveRule { // 手机号1开头第二位3-9后9位数字前后不能紧跟数字 public static final Pattern MOBILE_PATTERN Pattern.compile((?!\\d)1[3-9]\\d{9}(?!\\d)); // 身份证17位数字数字或X前后不能紧跟数字 public static final Pattern ID_CARD_PATTERN Pattern.compile((?!\\d)\\d{17}[0-9Xx](?!\\d)); // 银行卡16-19位数字前后不能紧跟数字需配合Luhn校验 public static final Pattern BANK_CARD_PATTERN Pattern.compile((?!\\d)\\d{16,19}(?!\\d)); // 邮箱简化版符合常见邮箱形态 public static final Pattern EMAIL_PATTERN Pattern.compile([A-Za-z0-9._%-][A-Za-z0-9.-]\\.[A-Za-z]{2,}); }注意看手机号正则里的(?!\d)和(?!\d)这是零宽断言保证匹配到的数字串前后不是数字。别小看这两个断言没有它们一份文档里连续抄写的18位订单号中间截出一段11位的子串就会被误判成手机号。银行卡正则同理但是16到19位纯数字的匹配面太宽必须用Luhn算法兜底。public static boolean luhnCheck(String cardNo) { if (cardNo null || cardNo.length() 15 || cardNo.length() 19) { return false; } int sum 0; boolean doubleDigit false; for (int i cardNo.length() - 1; i 0; i--) { int digit cardNo.charAt(i) - 0; if (digit 0 || digit 9) { return false; } if (doubleDigit) { digit * 2; if (digit 9) { digit - 9; } } sum digit; doubleDigit !doubleDigit; } return sum % 10 0; }身份证校验位算法大家比较熟悉权重数组配合模11映射我不再贴代码。这里要强调一个经验加了校验算法之后误报率至少降一个数量级。我拿内部脱敏数据做了一次回归测试银行卡规则加Luhn校验前准确率不到百分之三十加完之后准确率提升到百分之九十五以上。正则只能保证“长得像”校验算法才能保证“算得上”。3. 核心检测链路编码实战3.1 上传入口与参数校验Controller层对外暴露上传检测接口接收MultipartFile第一步不是解析而是校验大小和文件是否为空。上传大小的上限根据业务场景设定我这里设了20MBSpringBoot里通过配置项控制spring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB入口校验这个小细节值得多说一句。如果不在Controller层拦截一个超大文件会直接进入Tika解析流程解析前要探测类型、要创建临时存储消耗大量内存和磁盘成本远超拦截本身。所以我在接口最开始就是if (file.isEmpty() || file.getSize() MAX_SIZE)直接驳回。Controller代码很简单RestController RequestMapping(/api/dlp) public class DlpController { PostMapping(/detect) public ResultDetectionResult detect(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(空文件); } if (file.getSize() MAX_FILE_SIZE) { return Result.error(文件过大); } DetectionResult result detectionService.detect(file); return Result.success(result); } }这里有两个隐藏的重要细节。第一DetectionResult返回给调用方时绝不能携带完整的原始命中文本文本只能携带脱敏后的文本片段不然检测接口本身就变成了一个批量导出敏感信息的后台通道。第二接口返回的响应结构要同时包含命中类型和命中位置方便前端在文件详情页展示预警标签也方便审计系统接入。我的响应结构大致是{ hit: true, rules: [MOBILE, ID_CARD], message: 检测到2类敏感信息, maskedSnippets: [ 客户联系方式138****0000请勿外传 ] }3.2 文件真实格式识别与解析业务层DetectionService承担了主要工作流程是拿到MultipartFile的输入流先用detectType识别真实格式再抽取文本最后喂给规则引擎。Service public class DetectionService { private final TextExtractionService extractionService; private final SensitiveRuleEngine ruleEngine; public DetectionResult detect(MultipartFile file) throws Exception { Metadata metadata new Metadata(); metadata.set(Metadata.RESOURCE_NAME_KEY, file.getOriginalFilename()); try (InputStream rawIn file.getInputStream()) { TikaInputStream tikaIn TikaInputStream.get(rawIn); String realType extractionService.detectType(tikaIn, metadata); if (!supportType(realType)) { return DetectionResult.unsupported(realType); } String text extractionService.extract(tikaIn, metadata); return ruleEngine.evaluate(text, realType); } } }这段代码里TikaInputStream.get(rawIn)是处理流重读问题的关键。先说一个现象我在第一版时直接用原始InputStream先调detect再调parse检测结果一直是空文本排查到后来发现是流被消费完了第二个方法拿到的是一个空流。后来把输入流包成TikaInputStream它能基于临时文件缓冲内容支持重复读取问题就解决了。supportType方法里需要过滤掉那些拿到文本也没有检测意义的类型比如图片、音频、视频。这里的处理策略是拒绝直接放行但也不拦截而是返回一个“跳过检测”的状态让上层业务决定是放行还是转人工。图片里的文字比如身份证照片、截图里的聊天记录Tika不会主动做OCR如果业务场景有硬需求得单独接OCR模块这是后话。SensitiveRuleEngine.evaluate是规则引擎的入口它会遍历注册好的规则集合对抽取出来的文本逐条匹配聚合结果。匹配过程中有一个性能细节一旦某一个规则命中了我会继续跑完所有规则把命中的敏感类型集合返回给上层因为告警内容需要知道“这份文件里有手机号又有身份证”而不仅仅是“命中或不命中”这对安全运营判断风险等级非常重要。3.3 命中判定与脱敏展示规则引擎的判定结果如果命中需要进入处置模块。我在设计时抽象了一个DispositionHandler接口有三种实现直接拦截返回错误码给调用方、只告警记录审计日志后放行、脱敏展示将命中内容打码后存入资料库。用策略模式而不是if-else硬编码的原因很简单不同文件类型在不同业务模块里的敏感性不同。简历里的手机号可能是正常求职信息但客服中心导出的文件里出现手机号几乎可以判定为不该上传的资料。脱敏这件事值得单独说道说道。检测服务在返回命中文本片段时要对手机号等敏感数据做掩码处理方法很简单public static String maskMobile(String text) { return text.replaceAll((?!\\d)1[3-9]\\d{9}(?!\\d), m - m.group().substring(0, 3) **** m.group().substring(7)); }这种脱敏必须在服务端完成绝不能把原文返回给前端再脱敏。我见过有人图省事把原始文本和命中位置一起返回让前端打星号结果前端渲染时如果出了bug页面F12就能看到完整手机号等于我们自己在给泄露打开一扇门。检测服务作为安全组件自身必须是安全可信赖的。异步处理也是这里的优化点。解析文本是一个耗时操作一个20MB的docx解析出来可能需要几百毫秒如果在请求线程里同步执行高并发下接口吞吐会变得非常差。我后来在服务内部把检测流程放到了独立的线程池里用CompletableFuture异步执行主流程只负责把任务入队、快速返回“检测中”的状态。调用方通过轮询或回调获取最终检测结果。线程池要单独配置不跟业务线程池混用避免一个上传大文件的用户占满所有业务线程。4. 常见问题与性能优化实录4.1 解析失败的典型场景与应对实测过程中Tika解析的失败场景五花八门这里列几个最有代表性的和处理建议。加密文档是最常见的一种。Tika遇到带密码的docx、pdf时会抛出EncryptedDocumentException或类似异常。这种情况下不能无脑拦截可能是业务人员自己的密码文件。我的处理办法是捕获异常后返回一个“加密文件跳过检测”的状态同时计入审计日志由安全团队决定是否需要人工排查。WPS老格式Doc也踩过坑。Tika的官方解析器对微软的OOXML格式支持得好但对某些老版本WPS生成的格式解析结果偶尔会丢内容或者乱码。我最后的处理方案是升级到标准解析器包它内置了更多兼容解析器实测下来老格式的文本抽取覆盖率大幅提升。如果你的环境里这类格式特别多一定要做一轮样本回归测试。扫描件PDF几乎抽不出文本因为内容是图片需要OCR。Tika本身不带OCR能力但它的设计允许通过OCR2Parser接入Tesseract。我没在生产环境上OCR原因很简单OCR对算力要求高而且在低质量扫描件上错误率不低直接放到DLP检测链路里会拖慢整个流程。我目前的处理策略是对扫描件单独打标走人工复核队列。解析结果乱码的问题多半出在编码识别上。Tika对常见编码识别率不低但某些老旧格式里如果编码是GB18030变体偶尔会出问题。我在封装抽取服务时遇到过解析一个txt文件乱码的现象排查后发现是文件本身存的编码跟系统默认编码不一致。解决方式是在Metadata里显式设置Metadata.CONTENT_ENCODING或者用Reader的方式传字符流。做安全检测时解析质量直接影响规则匹配的准确率宁可多花一点时间处理编码问题也不要等上线后被误报和漏报一起打脸。4.2 高并发与内存控制DLP服务上线后迎来的第一个大流量场景是某个活动页面上传量激增检测接口的RT肉眼可见地变长。这其实不是Tika慢而是同步解析把线程池占满了。我把流程切换为异步后接口响应从平均2.3秒降到了50毫秒左右真正的检测任务被丢到后台队列里慢慢消化。这里的核心经验是安全检测类组件绝对不能阻塞用户主流程。内存控制方面WriteOutContentHandler的50000字符限制救了不少命。之前没有这个限制时一个人恶意上传了几百MB的csv文件Tika会把全量文本都拉进内存服务的堆内存直接飙高连续几次Full GC。设置字符上限后超限的文档会被拒绝解析虽然极端情况下也可能误伤超大日志文件但相比服务崩溃的风险这个取舍是值得的。还有一个容易被忽视的点是临时文件的清理。TikaInputStream在包装大流时会在临时目录生成文件用完不关时间长了磁盘空间被占满。确保在用完TikaInputStream后调用close()或者写在try-with-resources里。我在这上面吃过亏测试环境跑了两个星期某台磁盘使用率告警排查半天发现临时目录里堆满了未关闭的Tika缓冲文件。4.3 误报率调优经验误报是DLP系统的头号公敌误报太多会让运营人员对警报麻木真正的泄露事件反而被淹没。我调误报的过程本质上就是在“精确识别”和“不要漏报”中间反复走钢丝。有几个经验值得直接落地。第一正则宁可少写也不要写宽。一个宽松的手机号正则可能匹配到电话号码、座机号、日期串。我后续把所有正则都加上了前后边界断言宽匹配的数量直接少了三分之一。第二能上校验算法的必须上。身份证校验位、银行卡Luhn算法这两个校验能把误报率打到很低的水平。第三引入白名单机制。规则引擎可以配置两类白名单一类是号段白名单比如某些企业内部测试用的号段另一类是内容白名单命中敏感规则但同时出现在白名单词库里的内容可以直接放行。这些规则通过配置文件维护业务团队可以自助调整不会每次都来找开发改代码。另外规则引擎的输出值最好是带置信度的而不是单纯是或否。我给每个规则设计了权重值置信度达到一定阈值才判为命中。比如单独出现一个手机号置信度是60%如果同一段文本里同时出现手机号和姓名关键词“联系人”置信度就推到85%。置信度机制让系统在单条规则误报的情况下综合判断时不会误伤正常文件。写在最后的实话这类系统做起来不难难在别把自己当安全研究专家先当好一个务实的工程师。我最大的体会是DLP系统永远没有“完全搞定”的那一天文件格式在变、敏感信息的形态在变、业务的场景也在变。把Tika的解析、规则引擎的匹配、处置策略的配置这几层解耦干净后面无论接入新的文件类型还是新增敏感规则都不至于推倒重来。另一个值得投入精力的方向是把检测结果做成数据看板按文件类型、敏感类型、命中率做统计分析让安全团队能持续调优规则。这个系统上线半年后我看后台数据才意识到真正被拦截最多的敏感信息不是身份证而是那种混在报表里的客户手机号成批流出所以规则随时要跟着真实威胁走不能闭门造车。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑