审计范围:后端全部 Java 代码、前端全部 Vue/JS 代码、配置文件、数据库 DDL
严重等级:🔴 严重 (Critical) · 🟠 高危 (High) · 🟡 中危 (Medium) · 🔵 低危 (Low) · ⚪ 建议 (Info)
文件:backend/src/main/resources/application.yml
问题描述:数据库密码、邮箱密码、JWT Secret Key 均以明文形式直接写在配置文件中。一旦代码仓库泄露,攻击者即可获取数据库和邮箱的完全访问权限,并伪造任意 JWT Token。
# 当前问题代码
spring:
datasource:
password: Cmt123456 # 数据库密码明文
mail:
password: Bosind2023 # 邮箱密码明文
jwt:
secret: HKG2fedqr1BmynJl... # JWT密钥明文
修复建议:
${DB_PASSWORD} 或 ${MAIL_PASSWORD} 替代明文.gitignore 中排除本地开发用的配置文件,或使用 application-local.yml 分离文件:application.yml L38、security/JwtUtil.java
问题描述:JWT 签名密钥直接写死在配置中,无定期轮换机制。一旦密钥泄露(如通过 Git 历史),攻击者可以永久伪造合法 Token 访问系统。
修复建议:
文件:controller/AuthController.java L21-24、service/impl/SysUserServiceImpl.java L35-48
问题描述:登录接口 /auth/login 无任何频率限制或账号锁定机制。攻击者可以无限次尝试暴力破解密码,尤其配合弱密码(如 admin123)时风险极高。
修复建议:
文件:service/impl/SysUserServiceImpl.java L50-61、security/JwtAuthenticationFilter.java
问题描述:
JwtAuthenticationFilter 每次请求都会查询数据库获取最新用户信息(loadUserByUsername),所以禁用用户实际上会立即生效(因为 LoginUser.isEnabled() 检查 status),但删除用户后 loadUserByUsername 会抛出 UsernameNotFoundException,Filter 没有捕获这个异常修复建议:
JwtAuthenticationFilter 中 loadUserByUsername 调用应捕获 UsernameNotFoundException,用户不存在时跳过认证设置// JwtAuthenticationFilter.java - 建议增加异常处理
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// ... 设置认证
} catch (UsernameNotFoundException e) {
// 用户已删除,Token 无效,不设置认证
}
文件:controller/UserController.java L24-26
问题描述:/user/list 和 /user/enabled-list 接口直接返回 SysUser 实体,响应 JSON 中包含 password 字段(虽然是 BCrypt 哈希值),属于不必要的信息泄露。
// 当前代码 - 直接返回包含密码的实体
@GetMapping("/list")
public Result<List<SysUser>> list() {
return Result.success(sysUserService.list());
}
修复建议:
UserVO 类,排除 password 字段@JsonIgnore 注解标记 password 字段@JsonView 按需控制序列化文件:dto/PasswordUpdateDTO.java、dto/UserDTO.java
问题描述:密码校验仅为 @NotBlank(不为空),允许设置 "1"、"a" 等极弱密码。
修复建议:
@Size(min = 8, message = "密码长度不能少于8位") 注解文件:config/SecurityConfig.java、所有 Controller
问题描述:除用户管理外,所有接口仅需"登录"即可访问。普通用户可以:
修复建议:
文件:config/SecurityConfig.java L61-70
问题描述:allowedOriginPatterns 设为 *(允许任意域名),生产环境可能被恶意网站利用进行 CSRF 攻击。
// 当前代码
config.setAllowedOriginPatterns(List.of("*")); // 允许所有来源
修复建议:
文件:所有 Controller
问题描述:所有 API 接口均无请求频率限制,面临以下风险:
修复建议:
文件:controller/ResourceTypeController.java L29-32、controller/ProviderController.java L29-32
问题描述:删除资源类型或服务商时,未检查是否还有资源在使用该字典项。删除后会导致:
typeId / providerId 指向不存在的记录typeNameMap.get(r.getTypeId()) 返回 null,前端显示空值修复建议:
// ResourceTypeController.java - 删除前检查引用
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
long count = resourceService.count(
new LambdaQueryWrapper<Resource>().eq(Resource::getTypeId, id));
if (count > 0) {
throw new BusinessException("该类型下还有 " + count + " 个资源,无法删除");
}
resourceTypeService.removeById(id);
return Result.success();
}
文件:service/impl/SysUserServiceImpl.java L87-89
问题描述:删除用户时未检查:
ownerId 指向不存在的用户,VO 组装时 ownerName 为 nullsysUserService.getById(resource.getOwnerId()) 返回 null,该资源的到期邮件将不再发送修复建议:
文件:service/impl/ResourceServiceImpl.java L251-294
问题描述:costStatByProject() 和 costStatByType() 两个方法都执行 resourceMapper.selectList(null) 全量查询所有资源到内存中进行 Stream 分组聚合。当资源数量增长到万级时,会造成:
修复建议:
-- 改为 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
文件:dto/ResourceDTO.java、service/impl/ResourceServiceImpl.java
问题描述:资源表单中 expireDate(到期日期)可以早于 startDate(开通日期),系统不做校验,导致逻辑上的"已开通但已过期"异常数据。
修复建议:
if (expireDate != null && startDate != null && expireDate.isBefore(startDate)) throw new BusinessException("到期日期不能早于开通日期")文件:service/impl/OperationLogServiceImpl.java L16-24
问题描述:record() 方法通过 SecurityUtils.getCurrentUserId() 获取操作人 ID。在以下场景中会返回 null:
虽然当前定时任务未调用 record(),但如果后续在 refreshExpireStatus() 中增加日志记录,会导致 user_id 为 null 违反数据库非空约束。
修复建议:
operation_log.user_id 字段改为允许 NULL(或使用一个系统用户 ID 如 0)record() 方法增加 userId 参数,调用方显式传入文件:service/impl/ResourceServiceImpl.java L215-222、service/impl/ProjectServiceImpl.java L122-128
问题描述:resource 和 project 表使用逻辑删除(deleted 字段),但删除时对应的 project_resource 关联记录是物理删除。如果后续恢复已删除的资源/项目,关联关系已经永久丢失。
修复建议:
文件:
views/project/ProjectDetail.vue L89-91:pageSize: 1000 加载全部资源views/resource/ResourceForm.vue L138-145:每次打开弹窗加载 4 个接口的全量数据views/resource/ResourceList.vue L160-164:页面加载时全量加载类型和用户列表问题描述:多处使用全量加载方式获取下拉选项数据。虽然代码注释中提到"内部系统数据量级不大",但随着业务增长会成为瓶颈。
修复建议:
el-select 支持 remote + remote-method)文件:service/impl/DashboardServiceImpl.java L32-64
问题描述:工作台接口执行了 4 次数据库查询:
projectMapper.selectCount(null) — 项目总数resourceMapper.selectCount(null) — 资源总数resourceMapper.selectCount(status=2) — 即将到期数resourceMapper.selectCount(status=3) — 已过期数resourceMapper.selectList(status in 2,3) — 关注列表resourceMapper.selectList(null) — 全量资源用于类型分布统计可以合并优化为 1-2 次查询。
文件:application.yml L28-29、L49-51
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 每条 SQL 打印到 stdout
logging:
level:
com.example.resourceplatform: debug # debug 级别日志
问题描述:
修复建议:
application-dev.yml 开 debug + SQL 日志,application-prod.yml 关闭info 或 warn文件:frontend/vite.config.js L14
host: true, // 监听 0.0.0.0,局域网内其他设备也能访问
问题描述:开发服务器监听 0.0.0.0,在同一网络下的任何设备都可以访问开发环境,如果开发环境连接的是生产数据库则风险极高。
修复建议:仅在需要局域网调试时开启,平时使用默认的 localhost
文件:多处
问题描述:代码中大量使用魔法数字表示状态,分散在各处,容易不一致:
| 位置 | 含义 |
|---|---|
| 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)文件:views/project/ProjectList.vue L133
function openEdit(row) { Object.assign(form, row); formVisible.value = true }
问题描述:将列表行数据(包含 resourceCount、ownerName 等展示字段)整体拷贝到表单对象中。提交时这些额外字段会一并发送到后端。虽然后端使用 ProjectDTO 接收不会造成错误,但属于不规范的数据传递。
修复建议:
function openEdit(row) {
Object.assign(form, { id: row.id, name: row.name, ownerId: row.ownerId, status: row.status, remark: row.remark })
formVisible.value = true
}
文件:Dashboard.vue L78-80、ResourceList.vue L109-111、ProjectDetail.vue L79-81
问题描述:相同的 statusClass 函数在多个组件中重复定义,违反 DRY 原则。
修复建议:抽取为全局工具函数或 composable,如 utils/status.js。
文件:controller/OperationLogController.java、views/system/OperationLog.vue
问题描述:操作日志仅支持分页,不支持按模块、操作类型、操作人、时间范围等筛选,日志量增大后难以定位特定操作。
修复建议:增加查询参数:module、action、userId、startTime、endTime
文件:views/project/ProjectList.vue L143-150、views/CostStat.vue L41-49
问题描述:部分表单提交仅使用 try/finally 而无 catch,虽然 Axios 拦截器已做全局错误提示,但 finally 中重置 loading 状态可能在网络超时时产生误导。
修复建议:保持统一的 try/catch/finally 模式。
@EnableScheduling 在测试环境会触发定时任务文件:ResourcePlatformApplication.java L8
问题描述:@EnableScheduling 在应用启动时即生效,如果有集成测试启动完整 Spring Context,定时任务也会被触发执行。
修复建议:
@EnableScheduling 改为 @Profile("!test") 条件化启用@MockBean 替换定时任务所有数据库查询均使用 MyBatis-Plus 的 LambdaQueryWrapper 或 ServiceImpl 内置方法,参数通过预编译方式绑定,不存在 SQL 注入风险。
前端使用 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 |
第一优先级(立即修复):
第二优先级(近期迭代):
第三优先级(持续优化):