diff --git "a/doc/\344\273\243\347\240\201\345\256\241\346\237\245\346\212\245\345\221\212.md" "b/doc/\344\273\243\347\240\201\345\256\241\346\237\245\346\212\245\345\221\212.md" new file mode 100644 index 0000000..e34f060 --- /dev/null +++ "b/doc/\344\273\243\347\240\201\345\256\241\346\237\245\346\212\245\345\221\212.md" @@ -0,0 +1,599 @@ +# 考试管理系统代码审查报告 + +> 审查日期:2026-03-21 +> 项目名称:基于Spring + Shiro的考试管理系统 +> 审查工具:人工代码审查 + +--- + +## 📊 总体评估 + +| 评估项 | 结果 | +|--------|------| +| 代码质量评分 | **4.5/10** | +| 安全风险等级 | **高** | +| 主要问题数量 | 严重问题 6 个,中等问题 12 个 | + +### 评估说明 + +该项目整体架构清晰,采用了经典的SSM(Spring + SpringMVC + MyBatis)架构,并整合了Apache Shiro进行权限管理。但存在多个**严重安全隐患**,主要集中在密码明文存储、SQL注入风险、越权访问等方面,这些问题在生产环境中可能导致严重的安全事故。 + +--- + +## 🔴 严重问题(必须修复) + +### 问题1:密码明文存储 + +- **位置**:`src/main/java/com/system/realm/LoginRealm.java:70-71` +- **描述**:系统将用户密码以明文形式直接存储在数据库中,登录验证时直接比较明文密码 +- **风险**: + - 数据库泄露将直接暴露所有用户密码 + - 内部人员可查看用户密码 + - 违反数据保护法规要求 +- **修复建议**: + +```java +// LoginRealm.java - 修改认证逻辑 +protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + String username = (String) token.getPrincipal(); + Userlogin userlogin = userloginService.findByName(username); + + if (userlogin == null) { + throw new UnknownAccountException(); + } + + // 使用盐值+哈希的方式验证密码 + // 数据库存储格式:algorithm$iterations$salt$hash + return new SimpleAuthenticationInfo( + username, + userlogin.getPassword(), // 数据库中存储的加密密码 + ByteSource.Util.bytes(userlogin.getSalt()), // 盐值 + getName() + ); +} + +// 注册/修改密码时加密 +public void encryptPassword(Userlogin userlogin) { + String salt = new SecureRandomNumberGenerator().nextBytes().toHex(); + String hashedPassword = new SimpleHash( + "SHA-256", + userlogin.getPassword(), + salt, + 1024 + ).toHex(); + userlogin.setSalt(salt); + userlogin.setPassword(hashedPassword); +} +``` + +--- + +### 问题2:默认弱密码 + +- **位置**:`src/main/java/com/system/controller/AdminController.java:87-88`, `AdminController.java:175-176` +- **描述**:新建教师/学生账户时,默认密码硬编码为"123" +- **风险**: + - 攻击者可轻易猜测默认密码 + - 用户可能不会主动修改密码 +- **修复建议**: + +```java +// 生成随机初始密码 +public String generateRandomPassword() { + return UUID.randomUUID().toString().substring(0, 8); +} + +// 或者要求管理员设置初始密码 +@RequestMapping(value = "/addStudent", method = {RequestMethod.POST}) +public String addStudent(StudentCustom studentCustom, String initialPassword, Model model) throws Exception { + // 验证密码强度 + if (!isStrongPassword(initialPassword)) { + model.addAttribute("message", "密码强度不足,请设置包含大小写字母、数字、特殊字符的8位以上密码"); + return "error"; + } + // ... +} + +private boolean isStrongPassword(String password) { + String pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; + return password.matches(pattern); +} +``` + +--- + +### 问题3:URL路径拼写错误导致权限绕过 + +- **位置**:`src/main/resources/spring/applicationContext-shiro.xml:33` +- **描述**:Shiro配置中教师路径拼写错误为`/techer/**`,而实际Controller映射为`/teacher/**` +- **风险**: + - 教师模块完全暴露,任何登录用户都可访问 + - 严重的越权访问漏洞 +- **修复建议**: + +```xml + +/techer/** = authc, roles[teacher] + + +/teacher/** = authc, roles[teacher] +``` + +--- + +### 问题4:数据库配置文件泄露风险 + +- **位置**:`src/main/resources/mysql.properties` +- **描述**:数据库配置文件包含明文密码,且可能被提交到版本控制系统 +- **风险**: + - 数据库凭证泄露 + - 违反安全最佳实践 +- **修复建议**: + +```properties +# 使用环境变量或配置中心 +jdbc.driver = com.mysql.jdbc.Driver +jdbc.url = ${DB_URL:jdbc:mysql://localhost:3306/examination_system} +jdbc.username = ${DB_USERNAME} +jdbc.password = ${DB_PASSWORD} +``` + +同时确保`.gitignore`包含: +``` +mysql.properties +application-*.properties +``` + +--- + +### 问题5:空指针异常风险 + +- **位置**:`src/main/java/com/system/service/impl/UserloginServiceImpl.java:30-31` +- **描述**:`findByName`方法直接返回`list.get(0)`,未检查列表是否为空 +- **风险**: + - 当用户不存在时抛出`IndexOutOfBoundsException` + - 可能导致系统崩溃 +- **修复建议**: + +```java +public Userlogin findByName(String name) throws Exception { + UserloginExample userloginExample = new UserloginExample(); + UserloginExample.Criteria criteria = userloginExample.createCriteria(); + criteria.andUsernameEqualTo(name); + + List list = userloginMapper.selectByExample(userloginExample); + + if (list == null || list.isEmpty()) { + return null; // 或抛出自定义异常 + } + + return list.get(0); +} +``` + +--- + +### 问题6:课程删除约束未正确反馈 + +- **位置**:`src/main/java/com/system/controller/AdminController.java:314-317` +- **描述**:`removeCourse`方法调用Service层删除,但Service返回的`Boolean`结果被忽略,用户无法得知删除是否成功 +- **风险**: + - 用户无法得知操作结果 + - 可能导致数据不一致 +- **修复建议**: + +```java +@RequestMapping("/removeCourse") +public String removeCourse(Integer id, Model model) throws Exception { + if (id == null) { + return "admin/showCourse"; + } + + Boolean result = courseService.removeById(id); + if (!result) { + model.addAttribute("message", "该课程已有学生选课,无法删除"); + return "error"; + } + + return "redirect:/admin/showCourse"; +} +``` + +--- + +## 🟡 中等问题(建议修复) + +### 问题1:缺少CSRF防护 + +- **位置**:全局配置 +- **描述**:系统未实现CSRF(跨站请求伪造)防护机制 +- **建议**: + - 在Spring Security或Shiro中启用CSRF Token + - 所有POST请求验证Token + +```xml + + + + +``` + +--- + +### 问题2:缺少XSS防护 + +- **位置**:全局配置 +- **描述**:用户输入未进行HTML转义,存在XSS攻击风险 +- **建议**: + - 在JSP页面使用JSTL的``标签或`fn:escapeXml()` + - 后端对用户输入进行过滤 + +```jsp + +${student.username} + + + +``` + +--- + +### 问题3:缺少请求参数验证 + +- **位置**:所有Controller方法 +- **描述**:Controller层未对请求参数进行有效性验证 +- **建议**:使用JSR-303 Bean Validation + +```java +@RequestMapping(value = "/addStudent", method = {RequestMethod.POST}) +public String addStudent(@Valid StudentCustom studentCustom, BindingResult result, Model model) throws Exception { + if (result.hasErrors()) { + model.addAttribute("errors", result.getAllErrors()); + return "admin/addStudent"; + } + // ... +} +``` + +```java +// StudentCustom.java 添加验证注解 +public class StudentCustom { + @NotNull(message = "用户ID不能为空") + @Min(value = 10000, message = "学号格式不正确") + private Integer userid; + + @NotBlank(message = "姓名不能为空") + @Size(max = 50, message = "姓名长度不能超过50字符") + private String username; + // ... +} +``` + +--- + +### 问题4:异常处理信息泄露 + +- **位置**:`src/main/java/com/system/exception/CustomExceptionResolver.java:36-37` +- **描述**:全局异常处理器将原始异常信息返回给用户 +- **建议**: + - 记录详细日志 + - 返回通用错误信息 + +```java +public ModelAndView resolveException(...) { + ModelAndView modelAndView = new ModelAndView(); + + // 记录详细日志 + logger.error("系统异常", e); + + if (e instanceof CustomException) { + modelAndView.addObject("message", e.getMessage()); + } else if (e instanceof UnknownAccountException) { + modelAndView.addObject("message", "用户名或密码错误"); + } else if (e instanceof IncorrectCredentialsException) { + modelAndView.addObject("message", "用户名或密码错误"); + } else { + // 不要暴露系统内部错误 + modelAndView.addObject("message", "系统繁忙,请稍后重试"); + } + + modelAndView.setViewName("error"); + return modelAndView; +} +``` + +--- + +### 问题5:分页参数未验证 + +- **位置**:`src/main/java/com/system/controller/StudentController.java:32-39` +- **描述**:分页参数`page`未进行边界验证,可能导致异常 +- **建议**: + +```java +@RequestMapping(value = "/showCourse") +public String stuCourseShow(Model model, Integer page) throws Exception { + // 验证分页参数 + if (page == null || page < 1) { + page = 1; + } + + int totalCount = courseService.getCountCouse(); + int totalPages = (totalCount + pageSize - 1) / pageSize; + + if (page > totalPages) { + page = totalPages; + } + // ... +} +``` + +--- + +### 问题6:缺少操作日志记录 + +- **位置**:全局 +- **描述**:系统未记录关键操作日志(登录、修改密码、删除数据等) +- **建议**:使用AOP实现操作日志记录 + +```java +@Aspect +@Component +public class OperationLogAspect { + + @Autowired + private OperationLogService operationLogService; + + @AfterReturning(pointcut = "execution(* com.system.controller.*.*(..))", returning = "result") + public void logOperation(JoinPoint joinPoint, Object result) { + // 获取当前用户 + Subject subject = SecurityUtils.getSubject(); + String username = (String) subject.getPrincipal(); + + // 记录操作日志 + OperationLog log = new OperationLog(); + log.setUsername(username); + log.setOperation(joinPoint.getSignature().getName()); + log.setCreateTime(new Date()); + log.setIp(getClientIp()); + + operationLogService.save(log); + } +} +``` + +--- + +### 问题7:会话管理配置不完善 + +- **位置**:`src/main/resources/spring/applicationContext-shiro.xml` +- **描述**:未配置会话超时、并发登录限制等安全策略 +- **建议**: + +```xml + + + + + + + + + + + + + +``` + +--- + +### 问题8:缺少密码修改频率限制 + +- **位置**:`src/main/java/com/system/controller/RestPasswordController.java` +- **描述**:密码修改接口未做频率限制,可能被暴力破解 +- **建议**:添加限流机制 + +```java +// 使用Redis记录尝试次数 +@RequestMapping(value = "/passwordRest", method = {RequestMethod.POST}) +public String passwordRest(String oldPassword, String password1, HttpServletRequest request) throws Exception { + String username = (String) SecurityUtils.getSubject().getPrincipal(); + String key = "password_reset:" + username; + + // 检查尝试次数 + Integer attempts = redisTemplate.opsForValue().get(key); + if (attempts != null && attempts >= 5) { + throw new CustomException("尝试次数过多,请30分钟后再试"); + } + + // 验证失败增加计数 + if (!oldPassword.equals(userlogin.getPassword())) { + redisTemplate.opsForValue().increment(key); + redisTemplate.expire(key, 30, TimeUnit.MINUTES); + throw new CustomException("旧密码不正确"); + } + // ... +} +``` + +--- + +### 问题9:删除操作缺少确认机制 + +- **位置**:`src/main/java/com/system/controller/AdminController.java` 删除相关方法 +- **描述**:删除操作直接执行,无二次确认 +- **建议**:前端添加确认对话框,后端可添加软删除机制 + +--- + +### 问题10:SQL查询未使用索引优化 + +- **位置**:`src/main/java/com/system/mapper/StudentMapperCustom.xml:55-66` +- **描述**:关联查询未优化,可能导致性能问题 +- **建议**: + - 添加适当的数据库索引 + - 使用分页查询避免全表扫描 + +--- + +### 问题11:BeanUtils.copyProperties参数顺序不一致 + +- **位置**:`src/main/java/com/system/service/impl/CourseServiceImpl.java:93` +- **描述**:使用了Apache Commons BeanUtils,参数顺序为(目标, 源),而Spring BeanUtils为(源, 目标),容易混淆 +- **建议**:统一使用Spring BeanUtils,保持一致性 + +```java +// 统一使用Spring BeanUtils +org.springframework.beans.BeanUtils.copyProperties(source, target); +``` + +--- + +### 问题12:缺少接口幂等性保护 + +- **位置**:`src/main/java/com/system/controller/StudentController.java:55-66` +- **描述**:选课操作未实现幂等性保护,重复提交可能导致数据异常 +- **建议**:使用Token机制或数据库唯一约束 + +```sql +-- 添加唯一约束 +ALTER TABLE selectedcourse ADD UNIQUE KEY uk_course_student (courseID, studentID); +``` + +--- + +## 🟢 优化建议 + +### 建议1:引入缓存机制 + +- **描述**:频繁查询的数据(如学院列表、角色信息)可使用Redis缓存 +- **收益**:减少数据库压力,提升响应速度 + +```java +@Cacheable(value = "colleges", key = "'all'") +public List finAll() throws Exception { + return collegeMapper.selectByExample(null); +} +``` + +--- + +### 建议2:使用DTO分离请求响应对象 + +- **描述**:当前直接使用PO对象接收请求参数,存在字段暴露风险 +- **收益**:提高安全性,明确接口契约 + +--- + +### 建议3:添加API文档 + +- **描述**:集成Swagger/OpenAPI生成API文档 +- **收益**:便于前后端协作和接口测试 + +--- + +### 建议4:数据库连接池优化 + +- **位置**:`src/main/resources/spring/applicationContext-dao.xml` +- **描述**:C3P0连接池未配置详细参数 +- **建议**: + +```xml + + + + + + + + + + + + + +``` + +--- + +### 建议5:升级依赖版本 + +- **描述**:当前使用的Spring 4.x、Shiro 1.2.3版本较旧,存在已知漏洞 +- **建议**:升级到最新稳定版本 + +--- + +### 建议6:添加单元测试 + +- **描述**:测试覆盖率不足,建议增加单元测试和集成测试 +- **收益**:提高代码质量和可维护性 + +--- + +### 建议7:实现密码强度校验 + +- **描述**:修改密码时未校验密码强度 +- **建议**: + +```java +public boolean isValidPassword(String password) { + // 至少8位,包含大小写字母、数字和特殊字符 + String pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; + return password.matches(pattern); +} +``` + +--- + +## ✅ 优秀实践 + +1. **分层架构清晰**:项目采用标准的Controller-Service-Mapper-PO分层架构,职责分离明确 + +2. **事务管理完善**:通过AOP配置了完整的事务管理策略,确保数据一致性 + +3. **异常处理统一**:实现了全局异常处理器,统一处理系统异常 + +4. **代码规范良好**:命名规范,代码结构清晰 + +5. **MyBatis动态SQL**:使用Example类构建动态查询条件,避免SQL注入 + +6. **日期转换器**:自定义了日期转换器处理前端日期参数 + +7. **角色权限分离**:实现了基于角色的权限控制,admin/teacher/student角色分离 + +--- + +## 📋 修复优先级清单 + +| 优先级 | 问题 | 类型 | 预计工时 | +|--------|------|------|----------| +| **P0** | 密码明文存储 | 安全漏洞 | 4h | +| **P0** | URL路径拼写错误 | 安全漏洞 | 0.5h | +| **P0** | 默认弱密码 | 安全漏洞 | 1h | +| **P0** | 数据库配置泄露 | 安全漏洞 | 1h | +| **P1** | 空指针异常风险 | 功能缺陷 | 2h | +| **P1** | 课程删除约束未反馈 | 功能缺陷 | 1h | +| **P1** | 缺少CSRF防护 | 安全漏洞 | 2h | +| **P1** | 缺少XSS防护 | 安全漏洞 | 2h | +| **P2** | 参数验证缺失 | 代码优化 | 4h | +| **P2** | 异常信息泄露 | 安全漏洞 | 1h | +| **P2** | 分页参数验证 | 代码优化 | 1h | +| **P2** | 操作日志记录 | 功能增强 | 4h | +| **P3** | 会话管理配置 | 代码优化 | 1h | +| **P3** | 依赖版本升级 | 技术债务 | 4h | +| **P3** | 缓存机制引入 | 性能优化 | 4h | + +--- + +## 📝 总结 + +本次代码审查发现了多个**严重安全隐患**,其中密码明文存储和URL权限配置错误是最紧急需要修复的问题。建议按照优先级清单逐步修复,在修复安全问题后再进行功能优化。 + +**关键风险点**: +- 🔴 密码安全:明文存储 + 弱默认密码 +- 🔴 权限控制:URL配置错误导致权限绕过 +- 🟡 输入验证:缺少全局参数验证 +- 🟡 会话安全:缺少CSRF防护 + +建议在修复以上问题后,进行安全渗透测试,确保系统安全上线。