# 🔐 问卷权限问题分析报告 ## 📋 问题描述 **用户反馈**:登录系统后能看到所有问卷,明明没有给用户分配问卷权限。 **影响范围**:所有普通用户都能看到系统中的所有问卷,存在严重的权限控制漏洞。 ## 🔍 问题根源 ### 1. 问卷列表接口缺少权限控制 **文件**:`ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQuestionnaireController.java` ```java /** * 获取问卷列表(答题用户可访问) */ @GetMapping("/list") public TableDataInfo list(PsyQuestionnaire questionnaire) { startPage(); List list = questionnaireService.selectQuestionnaireList(questionnaire); return getDataTable(list); } ``` **问题点**: - ❌ **没有 `@PreAuthorize` 权限注解** - ❌ **没有权限过滤逻辑** - ❌ **直接返回所有问卷数据** - ❌ **任何登录用户都可以访问** ### 2. 对比:量表接口有完整的权限控制 **文件**:`ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyScaleController.java` ```java /** * 获取量表列表(包含问卷) * 允许管理员和学员访问 */ @PreAuthorize("@ss.hasPermi('psychology:scale:list') or @ss.hasAnyRoles('student')") @GetMapping("/list") public TableDataInfo list(PsyScale scale, @RequestParam(required = false, defaultValue = "true") Boolean includeQuestionnaire) { Set allowedScaleIds = resolveAllowedScaleIdsForCurrentUser(); Set restrictedScaleIds = resolveRestrictedScaleIds(); boolean needPermissionFilter = allowedScaleIds != null; // ... 权限过滤逻辑 ... } ``` **优点**: - ✅ 有 `@PreAuthorize` 权限注解 - ✅ 有完整的权限过滤逻辑 - ✅ 根据用户权限返回数据 - ✅ 支持管理员和普通用户的不同权限 ### 3. 权限过滤逻辑说明 在 `PsyScaleController` 中,问卷的处理逻辑是: ```java if ("questionnaire".equalsIgnoreCase(sourceType)) { boolean restricted = restrictedScaleIds != null && restrictedScaleIds.contains(scaleId); if (!restricted) { filtered.add(scale); // 只要不在限制列表中,就添加 continue; } } ``` **这段代码的含义**: - **问卷采用黑名单机制**:默认所有人可见,除非该问卷配置了权限 - **量表采用白名单机制**:只有分配了权限的用户才能看到 **问题**: - 即使量表接口有权限过滤,但前端可以直接调用问卷接口 `/psychology/questionnaire/list`,完全绕过权限控制 ## 📊 权限控制对比 | 项目 | 问卷接口 | 量表接口 | |------|---------|---------| | **接口路径** | `/psychology/questionnaire/list` | `/psychology/scale/list` | | **权限注解** | ❌ 无 | ✅ `@PreAuthorize` | | **权限过滤** | ❌ 无 | ✅ 完整逻辑 | | **访问控制** | ❌ 所有人可见 | ✅ 根据权限过滤 | | **安全性** | ❌ 严重漏洞 | ✅ 安全 | ## 🔧 修复方案 ### 方案一:为问卷接口添加权限控制(推荐) 修改 `PsyQuestionnaireController.java`,添加完整的权限控制逻辑。 **优点**: - ✅ 问卷和量表使用统一的权限机制 - ✅ 安全性高,符合最小权限原则 - ✅ 可以精确控制每个用户看到的问卷 **实现步骤**: 1. 在 `PsyQuestionnaireController` 中注入 `IPsyScalePermissionService` 2. 添加 `@PreAuthorize` 权限注解 3. 实现权限过滤逻辑(参考 `PsyScaleController`) 4. 修改 `/list` 接口,根据用户权限返回问卷列表 ### 方案二:问卷永久对所有人开放(不推荐) 如果业务上问卷确实需要对所有人开放(不需要权限控制),需要: 1. 明确文档说明:问卷对所有登录用户开放 2. 确保量表接口的权限过滤逻辑不影响问卷显示 3. 前端统一使用量表接口(`/psychology/scale/list?includeQuestionnaire=true`) **问题**: - ⚠️ 无法精确控制用户可见的问卷 - ⚠️ 可能暴露敏感内容 ## ✅ 推荐修复代码 ### 修改 `PsyQuestionnaireController.java` ```java package com.ddnai.web.controller.psychology; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import com.ddnai.common.annotation.Log; import com.ddnai.common.core.controller.BaseController; import com.ddnai.common.core.domain.AjaxResult; import com.ddnai.common.core.page.TableDataInfo; import com.ddnai.common.enums.BusinessType; import com.ddnai.common.utils.SecurityUtils; import com.ddnai.system.domain.psychology.PsyQuestionnaire; import com.ddnai.system.service.psychology.IPsyQuestionnaireService; import com.ddnai.system.service.psychology.IPsyScalePermissionService; @RestController @RequestMapping("/psychology/questionnaire") public class PsyQuestionnaireController extends BaseController { @Autowired private IPsyQuestionnaireService questionnaireService; @Autowired private IPsyScalePermissionService scalePermissionService; /** * 获取问卷列表(带权限控制) */ @PreAuthorize("@ss.hasPermi('psychology:questionnaire:list') or @ss.hasAnyRoles('student')") @GetMapping("/list") public TableDataInfo list(PsyQuestionnaire questionnaire) { // 获取当前用户权限 Set allowedScaleIds = resolveAllowedScaleIdsForCurrentUser(); Set restrictedScaleIds = resolveRestrictedScaleIds(); // 查询所有问卷 startPage(); List list = questionnaireService.selectQuestionnaireList(questionnaire); // 权限过滤 list = filterQuestionnaireListByPermission(list, allowedScaleIds, restrictedScaleIds); return getDataTable(list); } /** * 根据用户权限过滤问卷列表 */ private List filterQuestionnaireListByPermission( List list, Set allowedScaleIds, Set restrictedScaleIds) { // 管理员或无需权限过滤 if (allowedScaleIds == null || list == null) { return list; } List filtered = new ArrayList<>(); for (PsyQuestionnaire questionnaire : list) { if (questionnaire == null) { continue; } // 使用负数ID标识问卷(与量表接口保持一致) Long scaleId = -questionnaire.getQuestionnaireId(); // 如果该问卷未配置权限,对所有人开放 boolean restricted = restrictedScaleIds != null && restrictedScaleIds.contains(scaleId); if (!restricted) { filtered.add(questionnaire); continue; } // 如果配置了权限,检查用户是否有权限 if (allowedScaleIds.contains(scaleId)) { filtered.add(questionnaire); } } return filtered; } /** * 解析当前用户可访问的量表/问卷ID集合 * @return null 表示无需权限过滤(管理员);非null 表示必须过滤 */ private Set resolveAllowedScaleIdsForCurrentUser() { try { Long currentUserId = SecurityUtils.getUserId(); // 管理员拥有所有权限 if (currentUserId == null || currentUserId.equals(1L)) { return null; } // 检查是否有管理权限 boolean hasManagePerm = false; try { hasManagePerm = SecurityUtils.hasPermi("psychology:questionnaire:list"); } catch (Exception ignore) { // 忽略权限判断异常 } if (hasManagePerm) { return null; } // 获取用户有权限访问的量表/问卷ID List scaleIds = scalePermissionService.selectScaleIdsByUserId(currentUserId); return new HashSet<>(scaleIds != null ? scaleIds : new ArrayList<>()); } catch (Exception e) { logger.warn("获取用户问卷权限失败: {}", e.getMessage()); return null; } } /** * 获取所有已配置权限的量表/问卷ID */ private Set resolveRestrictedScaleIds() { try { List ids = scalePermissionService.selectAllScaleIdsWithPermission(); if (ids == null || ids.isEmpty()) { return java.util.Collections.emptySet(); } return new HashSet<>(ids); } catch (Exception e) { logger.warn("解析问卷权限限制列表失败: {}", e.getMessage()); return java.util.Collections.emptySet(); } } // ... 其他方法保持不变 ... } ``` ## 🧪 测试验证 ### 测试场景 1:普通用户(未分配权限) **预期**: - ✅ 只能看到未配置权限的问卷(公开问卷) - ❌ 不能看到已配置权限但未分配给该用户的问卷 ### 测试场景 2:普通用户(已分配权限) **预期**: - ✅ 能看到未配置权限的问卷 - ✅ 能看到已分配权限的问卷 - ❌ 不能看到未分配权限的其他问卷 ### 测试场景 3:管理员 **预期**: - ✅ 能看到所有问卷(不受权限限制) ### 测试步骤 1. **准备数据**: - 创建3个问卷:A(未配置权限)、B(已配置权限)、C(已配置权限) - 创建普通用户 user1,只分配问卷B的权限 2. **测试 user1 登录**: ```bash # 预期结果:只能看到问卷A和B,不能看到C GET /psychology/questionnaire/list ``` 3. **测试管理员登录**: ```bash # 预期结果:能看到所有问卷A、B、C GET /psychology/questionnaire/list ``` ## 📝 修改文件清单 - ✅ `ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQuestionnaireController.java` ## ⚠️ 注意事项 1. **权限分配兼容性**: - 问卷使用负数ID(-questionnaireId)存储在权限表中 - 与量表接口保持一致 2. **默认行为**: - 未配置权限的问卷对所有人开放(向后兼容) - 已配置权限的问卷只对有权限的用户开放 3. **前端调用**: - 建议统一使用量表接口:`/psychology/scale/list?includeQuestionnaire=true` - 或使用修复后的问卷接口:`/psychology/questionnaire/list` ## 🎯 修复后的效果 | 用户类型 | 修复前 | 修复后 | |---------|-------|-------| | **管理员** | 看到所有问卷 | 看到所有问卷 ✅ | | **普通用户(无权限)** | 看到所有问卷 ❌ | 只看到公开问卷 ✅ | | **普通用户(有权限)** | 看到所有问卷 ❌ | 看到公开+授权问卷 ✅ | --- **问题发现时间**:2024年12月2日 **严重程度**:⚠️ 高危(权限控制漏洞) **修复优先级**:🔥 紧急