模块:系统安全(后端鉴权 / 跨域 / AI 数据脱敏 / 日志脱敏) 测试人:Codex 日期:2026-09-14
针对本日安全自查发现的四类风险做加固(**这是安全整改,不是新功能**):
| 序号 | 原风险 | 本次处置 |
|---|---|---|
| 1 | 后端 /api/** 全部 permitAll,任何人无需登录即可调用,含"按报表期清空数据"等敏感接口 |
新增 JwtAuthFilter,除登录/验证码白名单外,/api/** 必须携带有效 Bearer 令牌,否则返回 JSON 401;application.yml 的 jwt.secret 由可猜测的固定串改为 64 位随机串 |
| 2 | CORS allowedOriginPattern("*") 且 allowCredentials(true),任意网站可携带凭证调用本机接口 |
改为只允许配置项 app.cors.allowed-origins 中列出的来源(默认 http://localhost:8080,http://127.0.0.1:8080),并关闭 allowCredentials |
| 3 | AI 查库接口可传 columns:"*" 放开整表,企业名称、统一社会信用代码、单位负责人、填表人、联系电话明文发往 DeepSeek |
① 去掉 "*" 放开整表的口子(改为只返回默认列);② 屏蔽 7 个字段(unified_credit_code/contact_person/contact_phone/unit_leader/stats_leader/report_unit/enterprise_code),既不可查也不可作过滤/排序条件,且不再出现在给模型的表结构提示里;③ 外发前把企业名称换成 Company1… 别名,自由文本中的企业名称与 11 位手机号一并处理,回答返回前端前还原 |
| 4 | MyBatis-Plus 默认 StdOutImpl 把整行数据(含企业名称、联系电话)打进 backend.log |
显式改为 Slf4jImpl,SQL 日志降为 DEBUG 级别,默认不输出 |
改动文件(8 改 1 新增):security/filter/JwtAuthFilter.java(新增)、security/config/SecurityConfig.java、common/config/CorsConfig.java、llmintegration/service/DbQueryExecutor.java、llmintegration/service/DesensitizeService.java、llmintegration/service/LLMService.java、traffic-audit-web/src/views/AuditResult.vue、traffic-audit-web/src/views/DataImport.vue、application.yml.example、docs/database.md。
连带修复:前端 AuditResult.vue(审核结果导出)与 DataImport.vue(下载导入模板)原本用 window.open('/api/...') 直连接口,浏览器跳转不会带 Authorization 头,开启鉴权后会 401;已改为走 axios 实例取 blob 再触发下载(报表生成页原本就是这种写法)。
mvn -o -q compile 通过,EXIT=0,重启后就绪)、前端 8080(dev 编译成功)。traffic_audit(MySQL 3308)。deepseek.api-url 临时指向本地假 DeepSeek 服务(127.0.0.1:18080),完整捕获每一次发往大模型的请求体,**测试后已恢复为真实地址并重启**。| 编号 | 用例 | 输入 | 预期 | 实际 | 结论 |
|---|---|---|---|---|---|
| S1 | 未登录访问业务接口 | 无令牌 GET /api/user/list、/api/data/list、/api/audit/export、/api/report/export/turnoverRank、/api/templates/...、/api/llm/chat |
全部 401 | 6 组接口全部返回 401 JSON | 通过 |
| S2 | 登录白名单不受影响 | 无令牌 GET /api/auth/captcha-config、POST /api/auth/login |
可访问 | 登录返回 200,令牌 156 位 | 通过(正向) |
| S3 | 携带令牌正常使用 | admin/123456 取令牌后调 /api/user/list、/api/audit/export、/api/report/export/turnoverRank |
200 | 200 / 200(59965 字节)/ 200(14589 字节) | 通过 |
| S4 | 令牌无效时拒绝 | 伪造/过期 Bearer |
401 | 401 | 通过(负向) |
| S5 | 跨域白名单生效 | 带 Origin: http://localhost:8080 请求 |
返回该来源 | 200,Access-Control-Allow-Origin: http://localhost:8080 |
通过 |
| S6 | 跨域非白名单被拦 | Origin: http://evil.example.com、http://127.0.0.1:9999 |
拒绝 | 均 403,未返回放行头 | 通过(负向) |
| S7 | 预检请求 | 无令牌 OPTIONS + Origin: localhost:8080 |
200 且带放行头 | 200,允许来源正确 | 通过 |
| S8 | AI 查全表被限制 | 让模型发起 columns:"*" 查询 h2032_enterprise_monthly |
只返回默认列 | 抓包显示返回列仅含默认列,**不含**任何联系人/电话/信用代码字段 | 通过 |
| S9 | AI 指定敏感列被拒 | columns:["enterpriseName","contactPhone","unifiedCreditCode"] |
拒绝 | 返回 该字段不允许查询: contactPhone |
通过(负向) |
| S10 | AI 用敏感列做过滤被拒 | filters:{"contactPhone":"180****8880"} |
拒绝 | 返回"列名不合法",可用列清单中已无联系人/电话/信用代码 | 通过(负向) |
| S11 | 企业名称脱敏外发 | 观察抓包中 tool 结果的 enterpriseName |
应为别名 | 实际为 Company1、Company2 |
通过 |
| S12 | 回答返回前还原 | AI 回答文案 | 显示真实企业名 | unmaskAll 还原(代码路径 + 抓包比对) |
通过 |
| S13 | 日志不再落整行数据 | 重启后执行登录、列表、导出、审核结果导出 | 无行转储 | <= 行 0 条、"Total:" 0 条、手机号样式 0 条(整改前单次查询即数百条) |
通过 |
| S14 | 前端编译 | 改动 2 个 .vue 后 dev 编译 | 无错误 | Compiled successfully ×2 |
通过 |
| S15 | 前端带令牌下载 | 审核结果导出、模板下载改为 blob 方式 | 不再 401 | 接口侧带令牌返回 200;前端编译通过(浏览器终验见"遗留") | 部分(页面待人工点验) |
StdOutImpl**,仅删除配置项无效,仍把整行数据打进日志。已显式改为 Slf4jImpl 并复测(S13)。window.open 会因鉴权失效:AuditResult.vue 的审核结果导出、DataImport.vue 的模板下载。已改为 axios 取 blob 后下载(原报表生成页的同款写法)。jwt.secret 可猜测:鉴权一旦真正启用,固定且写死的密钥等于可伪造令牌。已换成 64 位随机串(application.yml 未入库,仓库内只有占位符示例)。**副作用:所有已登录用户的令牌失效,需重新登录。**llm_desensitize_map 表与实际实现不符:该表设计为可逆脱敏映射落库,但代码从未引用(0 行)。本次采用内存态脱敏,已在 docs/database.md 第七章补充"实现说明",注明该表为预留、当前未启用,避免文档与实现不一致。四类风险已全部处置并通过接口级、产物级与"报文级"验证:未登录无法访问任何业务接口;跨域仅放行配置来源;发往 DeepSeek 的报文里企业名称已换为别名,联系人、电话、统一社会信用代码等字段完全无法被查询或外发;后端日志不再落整行业务数据。
遗留(不影响本次结论):
- 3D 验证码仍默认关闭(app.captcha.required: false),生产部署前建议开启;
- sys_role / sys_user_role 仍为空表,角色权限未落地,目前"权限"仍只等于"报表页显示哪几个模块";
- 审核结果导出、模板下载两处前端改动建议在浏览器点验一遍。