# 软件项目资源登记平台 — 代码审计报告 **审计范围**:后端全部 Java 代码、前端全部 Vue/JS 代码、配置文件、数据库 DDL **严重等级**:🔴 严重 (Critical) · 🟠 高危 (High) · 🟡 中危 (Medium) · 🔵 低危 (Low) · ⚪ 建议 (Info) --- ## 一、安全问题 ### AUDIT-001 🔴 配置文件明文存储敏感凭据 **文件**:`backend/src/main/resources/application.yml` **问题描述**:数据库密码、邮箱密码、JWT Secret Key 均以明文形式直接写在配置文件中。一旦代码仓库泄露,攻击者即可获取数据库和邮箱的完全访问权限,并伪造任意 JWT Token。 ```yaml # 当前问题代码 spring: datasource: password: Cmt123456 # 数据库密码明文 mail: password: Bosind2023 # 邮箱密码明文 jwt: secret: HKG2fedqr1BmynJl... # JWT密钥明文 ``` **修复建议**: - 使用环境变量 `${DB_PASSWORD}` 或 `${MAIL_PASSWORD}` 替代明文 - 生产环境使用配置中心(如 Nacos/Apollo)或 K8s Secret 管理敏感配置 - 在 `.gitignore` 中排除本地开发用的配置文件,或使用 `application-local.yml` 分离 --- ### AUDIT-002 🔴 JWT Secret 硬编码且无轮换机制 **文件**:`application.yml` L38、`security/JwtUtil.java` **问题描述**:JWT 签名密钥直接写死在配置中,无定期轮换机制。一旦密钥泄露(如通过 Git 历史),攻击者可以永久伪造合法 Token 访问系统。 **修复建议**: - 密钥从环境变量读取,且生产环境使用独立的强随机密钥 - 考虑引入 Token 黑名单机制(如 Redis 存储已注销的 Token) - 缩短 Token 有效期(当前 24 小时偏长,建议 2-4 小时 + Refresh Token 机制) --- ### AUDIT-003 🟠 无登录频率限制(暴力破解风险) **文件**:`controller/AuthController.java` L21-24、`service/impl/SysUserServiceImpl.java` L35-48 **问题描述**:登录接口 `/auth/login` 无任何频率限制或账号锁定机制。攻击者可以无限次尝试暴力破解密码,尤其配合弱密码(如 admin123)时风险极高。 **修复建议**: - 引入验证码机制(连续失败 N 次后要求验证码) - 实现 IP/账号维度的登录频率限制(如 Bucket4j、Redis + Lua 限流) - 连续失败 5 次后临时锁定账号 15-30 分钟 --- ### AUDIT-004 🟠 密码修改/用户禁用后旧 Token 未失效 **文件**:`service/impl/SysUserServiceImpl.java` L50-61、`security/JwtAuthenticationFilter.java` **问题描述**: - 修改密码后仅前端清除 Token,服务端未将旧 Token 加入黑名单,旧 Token 在过期前仍可使用 - 禁用/删除用户后,已发放的 Token 仍然有效直到自然过期 - `JwtAuthenticationFilter` 每次请求都会查询数据库获取最新用户信息(`loadUserByUsername`),所以**禁用用户实际上会立即生效**(因为 `LoginUser.isEnabled()` 检查 status),但**删除用户后** `loadUserByUsername` 会抛出 `UsernameNotFoundException`,Filter 没有捕获这个异常 **修复建议**: - `JwtAuthenticationFilter` 中 `loadUserByUsername` 调用应捕获 `UsernameNotFoundException`,用户不存在时跳过认证设置 - 修改密码时记录密码变更时间戳,Token 签发时间早于变更时间的视为无效 - 或引入 Redis Token 黑名单 ```java // JwtAuthenticationFilter.java - 建议增加异常处理 try { UserDetails userDetails = userDetailsService.loadUserByUsername(username); // ... 设置认证 } catch (UsernameNotFoundException e) { // 用户已删除,Token 无效,不设置认证 } ``` --- ### AUDIT-005 🟠 用户列表接口返回密码字段 **文件**:`controller/UserController.java` L24-26 **问题描述**:`/user/list` 和 `/user/enabled-list` 接口直接返回 `SysUser` 实体,响应 JSON 中包含 `password` 字段(虽然是 BCrypt 哈希值),属于不必要的信息泄露。 ```java // 当前代码 - 直接返回包含密码的实体 @GetMapping("/list") public Result> list() { return Result.success(sysUserService.list()); } ``` **修复建议**: - 创建 `UserVO` 类,排除 `password` 字段 - 或使用 `@JsonIgnore` 注解标记 password 字段 - 或使用 Jackson 的 `@JsonView` 按需控制序列化 --- ### AUDIT-006 🟠 无密码复杂度校验 **文件**:`dto/PasswordUpdateDTO.java`、`dto/UserDTO.java` **问题描述**:密码校验仅为 `@NotBlank`(不为空),允许设置 "1"、"a" 等极弱密码。 **修复建议**: - 添加 `@Size(min = 8, message = "密码长度不能少于8位")` 注解 - 后端增加密码复杂度校验(包含大小写字母 + 数字 + 特殊字符,至少满足 2 种) - 前端增加密码强度提示 --- ### AUDIT-007 🟠 无细粒度数据权限控制 **文件**:`config/SecurityConfig.java`、所有 Controller **问题描述**:除用户管理外,所有接口仅需"登录"即可访问。普通用户可以: - 删除其他人创建的资源/项目 - 查看所有资源(包括不属于自己负责的) - 修改其他人负责的项目信息 - 随意操作字典(新增/删除资源类型、服务商) **修复建议**: - 资源/项目的修改和删除操作应校验当前用户是否为负责人或管理员 - 字典管理建议限制为管理员操作 - 长期考虑实现 RBAC 权限模型 --- ### AUDIT-008 🟡 CORS 配置过于宽松 **文件**:`config/SecurityConfig.java` L61-70 **问题描述**:`allowedOriginPatterns` 设为 `*`(允许任意域名),生产环境可能被恶意网站利用进行 CSRF 攻击。 ```java // 当前代码 config.setAllowedOriginPatterns(List.of("*")); // 允许所有来源 ``` **修复建议**: - 生产环境改为具体的前端域名 - 通过配置文件管理允许的来源列表 --- ### AUDIT-009 🟡 无接口限流防护 **文件**:所有 Controller **问题描述**:所有 API 接口均无请求频率限制,面临以下风险: - 恶意用户大量调用导出接口消耗服务器资源 - 暴力请求分页接口进行数据爬取 - 资源列表导出可被用于 DDoS 攻击向量 **修复建议**: - 对敏感接口(导出、登录)添加限流注解 - 使用 Bucket4j 或 Guava RateLimiter 进行全局限流 - 考虑在 Nginx 层做请求限速 --- ## 二、业务逻辑缺陷 ### AUDIT-010 🟠 删除字典项无引用检查 **文件**:`controller/ResourceTypeController.java` L29-32、`controller/ProviderController.java` L29-32 **问题描述**:删除资源类型或服务商时,未检查是否还有资源在使用该字典项。删除后会导致: - 已有资源的 `typeId` / `providerId` 指向不存在的记录 - 资源列表的 VO 组装时 `typeNameMap.get(r.getTypeId())` 返回 null,前端显示空值 - 资源编辑时类型/服务商下拉选中项消失 **修复建议**: ```java // ResourceTypeController.java - 删除前检查引用 @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { long count = resourceService.count( new LambdaQueryWrapper().eq(Resource::getTypeId, id)); if (count > 0) { throw new BusinessException("该类型下还有 " + count + " 个资源,无法删除"); } resourceTypeService.removeById(id); return Result.success(); } ``` --- ### AUDIT-011 🟠 删除用户无关联检查 **文件**:`service/impl/SysUserServiceImpl.java` L87-89 **问题描述**:删除用户时未检查: - 该用户是否为某些资源/项目的负责人 - 删除后资源的 `ownerId` 指向不存在的用户,VO 组装时 `ownerName` 为 null - 到期提醒定时任务中 `sysUserService.getById(resource.getOwnerId())` 返回 null,该资源的到期邮件将不再发送 **修复建议**: - 删除前检查关联的资源/项目数量,提示用户先转移负责人 - 或改为"禁用"而非"删除" --- ### AUDIT-012 🟡 费用统计全量加载到内存计算 **文件**:`service/impl/ResourceServiceImpl.java` L251-294 **问题描述**:`costStatByProject()` 和 `costStatByType()` 两个方法都执行 `resourceMapper.selectList(null)` 全量查询所有资源到内存中进行 Stream 分组聚合。当资源数量增长到万级时,会造成: - 大量内存消耗(加载全部实体对象) - 数据库 I/O 压力(全表扫描) - GC 压力增大 **修复建议**: ```sql -- 改为 SQL 聚合查询 SELECT rt.name AS dimension_name, COUNT(r.id) AS resource_count, COALESCE(SUM(r.cost), 0) AS total_cost FROM resource r LEFT JOIN resource_type rt ON r.type_id = rt.id WHERE r.deleted = 0 GROUP BY rt.name ORDER BY total_cost DESC ``` --- ### AUDIT-013 🟡 无日期合理性校验 **文件**:`dto/ResourceDTO.java`、`service/impl/ResourceServiceImpl.java` **问题描述**:资源表单中 `expireDate`(到期日期)可以早于 `startDate`(开通日期),系统不做校验,导致逻辑上的"已开通但已过期"异常数据。 **修复建议**: - Service 层增加校验:`if (expireDate != null && startDate != null && expireDate.isBefore(startDate)) throw new BusinessException("到期日期不能早于开通日期")` --- ### AUDIT-014 🟡 操作日志 userId 可能为 null **文件**:`service/impl/OperationLogServiceImpl.java` L16-24 **问题描述**:`record()` 方法通过 `SecurityUtils.getCurrentUserId()` 获取操作人 ID。在以下场景中会返回 null: - 定时任务上下文中执行(无 HTTP 请求上下文) - 系统内部调用的批量操作 虽然当前定时任务未调用 `record()`,但如果后续在 `refreshExpireStatus()` 中增加日志记录,会导致 `user_id` 为 null 违反数据库非空约束。 **修复建议**: - `operation_log.user_id` 字段改为允许 NULL(或使用一个系统用户 ID 如 0) - `record()` 方法增加 `userId` 参数,调用方显式传入 --- ### AUDIT-015 🟡 项目/资源删除时关联表物理删除 **文件**:`service/impl/ResourceServiceImpl.java` L215-222、`service/impl/ProjectServiceImpl.java` L122-128 **问题描述**:`resource` 和 `project` 表使用逻辑删除(deleted 字段),但删除时对应的 `project_resource` 关联记录是**物理删除**。如果后续恢复已删除的资源/项目,关联关系已经永久丢失。 **修复建议**: - 不删除关联记录,改为在查询时根据两端的 deleted 状态过滤 - 或关联表也引入逻辑删除 --- ## 三、性能问题 ### AUDIT-016 🟡 前端全量加载下拉选项 **文件**: - `views/project/ProjectDetail.vue` L89-91:`pageSize: 1000` 加载全部资源 - `views/resource/ResourceForm.vue` L138-145:每次打开弹窗加载 4 个接口的全量数据 - `views/resource/ResourceList.vue` L160-164:页面加载时全量加载类型和用户列表 **问题描述**:多处使用全量加载方式获取下拉选项数据。虽然代码注释中提到"内部系统数据量级不大",但随着业务增长会成为瓶颈。 **修复建议**: - 对资源/项目选择改为远程搜索 + 分页加载(Element Plus 的 `el-select` 支持 `remote` + `remote-method`) - 对字典类数据(类型、服务商、用户)可引入前端缓存(Pinia Store + TTL) --- ### AUDIT-017 🔵 工作台仪表盘查询可优化 **文件**:`service/impl/DashboardServiceImpl.java` L32-64 **问题描述**:工作台接口执行了 4 次数据库查询: 1. `projectMapper.selectCount(null)` — 项目总数 2. `resourceMapper.selectCount(null)` — 资源总数 3. `resourceMapper.selectCount(status=2)` — 即将到期数 4. `resourceMapper.selectCount(status=3)` — 已过期数 5. `resourceMapper.selectList(status in 2,3)` — 关注列表 6. `resourceMapper.selectList(null)` — 全量资源用于类型分布统计 可以合并优化为 1-2 次查询。 --- ## 四、配置与部署问题 ### AUDIT-018 🟡 生产环境不应开启 SQL 日志和 debug 日志 **文件**:`application.yml` L28-29、L49-51 ```yaml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 每条 SQL 打印到 stdout logging: level: com.example.resourceplatform: debug # debug 级别日志 ``` **问题描述**: - SQL 日志输出到 stdout 会严重影响性能(同步 I/O),且可能泄露敏感数据(查询中的参数值) - debug 级别日志量大,产生大量磁盘 I/O **修复建议**: - 使用 Spring Profile 分离:`application-dev.yml` 开 debug + SQL 日志,`application-prod.yml` 关闭 - 生产环境日志级别设为 `info` 或 `warn` --- ### AUDIT-019 🔵 Vite 开发服务器 host: true 的安全风险 **文件**:`frontend/vite.config.js` L14 ```javascript host: true, // 监听 0.0.0.0,局域网内其他设备也能访问 ``` **问题描述**:开发服务器监听 `0.0.0.0`,在同一网络下的任何设备都可以访问开发环境,如果开发环境连接的是生产数据库则风险极高。 **修复建议**:仅在需要局域网调试时开启,平时使用默认的 `localhost` --- ## 五、代码质量问题 ### AUDIT-020 🔵 状态码使用魔法数字,缺乏枚举管理 **文件**:多处 **问题描述**:代码中大量使用魔法数字表示状态,分散在各处,容易不一致: | 位置 | 含义 | |------|------| | Resource.status | 1-使用中 2-即将到期 3-已过期 0-已停用 | | Project.status | 1-进行中 0-已下线 | | SysUser.role | 1-管理员 2-普通用户 | | SysUser.status | 1-启用 0-禁用 | | Resource.costCycle | 1-月付 2-年付 3-一次性 | | ExpireNotifyLog.notifyStage | 1-提前30天 2-提前7天 3-已过期 | **修复建议**: - 为每组状态值创建枚举类(如 `ResourceStatus`、`ProjectStatus`、`UserRole`) - 在 Entity 中使用枚举类型字段 - 前后端共享统一的常量定义 --- ### AUDIT-021 🔵 前端表单编辑时可能提交多余字段 **文件**:`views/project/ProjectList.vue` L133 ```javascript function openEdit(row) { Object.assign(form, row); formVisible.value = true } ``` **问题描述**:将列表行数据(包含 `resourceCount`、`ownerName` 等展示字段)整体拷贝到表单对象中。提交时这些额外字段会一并发送到后端。虽然后端使用 `ProjectDTO` 接收不会造成错误,但属于不规范的数据传递。 **修复建议**: ```javascript function openEdit(row) { Object.assign(form, { id: row.id, name: row.name, ownerId: row.ownerId, status: row.status, remark: row.remark }) formVisible.value = true } ``` --- ### AUDIT-022 🔵 前端多处重复的 statusClass 函数 **文件**:`Dashboard.vue` L78-80、`ResourceList.vue` L109-111、`ProjectDetail.vue` L79-81 **问题描述**:相同的 `statusClass` 函数在多个组件中重复定义,违反 DRY 原则。 **修复建议**:抽取为全局工具函数或 composable,如 `utils/status.js`。 --- ### AUDIT-023 🔵 操作日志缺少筛选功能 **文件**:`controller/OperationLogController.java`、`views/system/OperationLog.vue` **问题描述**:操作日志仅支持分页,不支持按模块、操作类型、操作人、时间范围等筛选,日志量增大后难以定位特定操作。 **修复建议**:增加查询参数:`module`、`action`、`userId`、`startTime`、`endTime` --- ### AUDIT-024 ⚪ 前端 handleSubmit 缺少 catch 错误处理 **文件**:`views/project/ProjectList.vue` L143-150、`views/CostStat.vue` L41-49 **问题描述**:部分表单提交仅使用 `try/finally` 而无 `catch`,虽然 Axios 拦截器已做全局错误提示,但 `finally` 中重置 loading 状态可能在网络超时时产生误导。 **修复建议**:保持统一的 `try/catch/finally` 模式。 --- ### AUDIT-025 ⚪ `@EnableScheduling` 在测试环境会触发定时任务 **文件**:`ResourcePlatformApplication.java` L8 **问题描述**:`@EnableScheduling` 在应用启动时即生效,如果有集成测试启动完整 Spring Context,定时任务也会被触发执行。 **修复建议**: - 将 `@EnableScheduling` 改为 `@Profile("!test")` 条件化启用 - 或在测试中使用 `@MockBean` 替换定时任务 --- ## 六、SQL 注入与数据访问安全 ### ✅ 无 SQL 注入风险 所有数据库查询均使用 MyBatis-Plus 的 `LambdaQueryWrapper` 或 `ServiceImpl` 内置方法,参数通过预编译方式绑定,**不存在 SQL 注入风险**。 ### ✅ 无 XSS 风险 前端使用 Vue 3 模板语法,默认对所有插值表达式进行 HTML 转义,且未使用 `v-html` 指令,**不存在 XSS 风险**。 ### ✅ 事务使用正确 `ResourceServiceImpl.saveOrUpdate()`、`delete()`、`relateToProject()` 和 `ProjectServiceImpl.saveOrUpdate()`、`delete()` 等涉及多表操作的方法均正确使用了 `@Transactional` 注解,保证数据一致性。 ### ✅ 全局异常处理完善 `GlobalExceptionHandler` 正确捕获了 `BusinessException`、参数校验异常、认证异常、权限异常和未知异常,不会将原始堆栈信息泄露给前端。 --- ## 七、审计结果汇总 | 严重等级 | 数量 | 编号 | | -------- | ---- | ------------------------------------- | | 🔴 严重 | 2 | AUDIT-001, AUDIT-002 | | 🟠 高危 | 7 | AUDIT-003~007, AUDIT-010, AUDIT-011 | | 🟡 中危 | 5 | AUDIT-008~009, AUDIT-012~014, AUDIT-018 | | 🔵 低危 | 5 | AUDIT-015~017, AUDIT-020~023 | | ⚪ 建议 | 2 | AUDIT-024, AUDIT-025 | ### 修复优先级建议 **第一优先级(立即修复)**: 1. AUDIT-001 — 敏感凭据外部化 2. AUDIT-002 — JWT 密钥安全 3. AUDIT-005 — 用户列表排除密码字段 4. AUDIT-004 — JwtAuthenticationFilter 异常处理 **第二优先级(近期迭代)**: 5. AUDIT-003 — 登录频率限制 6. AUDIT-006 — 密码复杂度校验 7. AUDIT-007 — 数据权限控制 8. AUDIT-010/011 — 删除操作引用检查 9. AUDIT-018 — 生产环境配置分离 **第三优先级(持续优化)**: 10. AUDIT-008/009 — CORS + 限流 11. AUDIT-012 — 费用统计性能优化 12. AUDIT-016 — 前端下拉远程搜索 13. 其余低危和建议项