11 KiB
11 KiB
🔐 问卷权限问题分析报告
📋 问题描述
用户反馈:登录系统后能看到所有问卷,明明没有给用户分配问卷权限。
影响范围:所有普通用户都能看到系统中的所有问卷,存在严重的权限控制漏洞。
🔍 问题根源
1. 问卷列表接口缺少权限控制
文件:ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQuestionnaireController.java
/**
* 获取问卷列表(答题用户可访问)
*/
@GetMapping("/list")
public TableDataInfo list(PsyQuestionnaire questionnaire)
{
startPage();
List<PsyQuestionnaire> list = questionnaireService.selectQuestionnaireList(questionnaire);
return getDataTable(list);
}
问题点:
- ❌ 没有
@PreAuthorize权限注解 - ❌ 没有权限过滤逻辑
- ❌ 直接返回所有问卷数据
- ❌ 任何登录用户都可以访问
2. 对比:量表接口有完整的权限控制
文件:ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyScaleController.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<Long> allowedScaleIds = resolveAllowedScaleIdsForCurrentUser();
Set<Long> restrictedScaleIds = resolveRestrictedScaleIds();
boolean needPermissionFilter = allowedScaleIds != null;
// ... 权限过滤逻辑 ...
}
优点:
- ✅ 有
@PreAuthorize权限注解 - ✅ 有完整的权限过滤逻辑
- ✅ 根据用户权限返回数据
- ✅ 支持管理员和普通用户的不同权限
3. 权限过滤逻辑说明
在 PsyScaleController 中,问卷的处理逻辑是:
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,添加完整的权限控制逻辑。
优点:
- ✅ 问卷和量表使用统一的权限机制
- ✅ 安全性高,符合最小权限原则
- ✅ 可以精确控制每个用户看到的问卷
实现步骤:
- 在
PsyQuestionnaireController中注入IPsyScalePermissionService - 添加
@PreAuthorize权限注解 - 实现权限过滤逻辑(参考
PsyScaleController) - 修改
/list接口,根据用户权限返回问卷列表
方案二:问卷永久对所有人开放(不推荐)
如果业务上问卷确实需要对所有人开放(不需要权限控制),需要:
- 明确文档说明:问卷对所有登录用户开放
- 确保量表接口的权限过滤逻辑不影响问卷显示
- 前端统一使用量表接口(
/psychology/scale/list?includeQuestionnaire=true)
问题:
- ⚠️ 无法精确控制用户可见的问卷
- ⚠️ 可能暴露敏感内容
✅ 推荐修复代码
修改 PsyQuestionnaireController.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<Long> allowedScaleIds = resolveAllowedScaleIdsForCurrentUser();
Set<Long> restrictedScaleIds = resolveRestrictedScaleIds();
// 查询所有问卷
startPage();
List<PsyQuestionnaire> list = questionnaireService.selectQuestionnaireList(questionnaire);
// 权限过滤
list = filterQuestionnaireListByPermission(list, allowedScaleIds, restrictedScaleIds);
return getDataTable(list);
}
/**
* 根据用户权限过滤问卷列表
*/
private List<PsyQuestionnaire> filterQuestionnaireListByPermission(
List<PsyQuestionnaire> list,
Set<Long> allowedScaleIds,
Set<Long> restrictedScaleIds)
{
// 管理员或无需权限过滤
if (allowedScaleIds == null || list == null)
{
return list;
}
List<PsyQuestionnaire> 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<Long> 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<Long> scaleIds = scalePermissionService.selectScaleIdsByUserId(currentUserId);
return new HashSet<>(scaleIds != null ? scaleIds : new ArrayList<>());
}
catch (Exception e)
{
logger.warn("获取用户问卷权限失败: {}", e.getMessage());
return null;
}
}
/**
* 获取所有已配置权限的量表/问卷ID
*/
private Set<Long> resolveRestrictedScaleIds()
{
try
{
List<Long> 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:管理员
预期:
- ✅ 能看到所有问卷(不受权限限制)
测试步骤
-
准备数据:
- 创建3个问卷:A(未配置权限)、B(已配置权限)、C(已配置权限)
- 创建普通用户 user1,只分配问卷B的权限
-
测试 user1 登录:
# 预期结果:只能看到问卷A和B,不能看到C GET /psychology/questionnaire/list -
测试管理员登录:
# 预期结果:能看到所有问卷A、B、C GET /psychology/questionnaire/list
📝 修改文件清单
- ✅
ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQuestionnaireController.java
⚠️ 注意事项
-
权限分配兼容性:
- 问卷使用负数ID(-questionnaireId)存储在权限表中
- 与量表接口保持一致
-
默认行为:
- 未配置权限的问卷对所有人开放(向后兼容)
- 已配置权限的问卷只对有权限的用户开放
-
前端调用:
- 建议统一使用量表接口:
/psychology/scale/list?includeQuestionnaire=true - 或使用修复后的问卷接口:
/psychology/questionnaire/list
- 建议统一使用量表接口:
🎯 修复后的效果
| 用户类型 | 修复前 | 修复后 |
|---|---|---|
| 管理员 | 看到所有问卷 | 看到所有问卷 ✅ |
| 普通用户(无权限) | 看到所有问卷 ❌ | 只看到公开问卷 ✅ |
| 普通用户(有权限) | 看到所有问卷 ❌ | 看到公开+授权问卷 ✅ |
问题发现时间:2024年12月2日
严重程度:⚠️ 高危(权限控制漏洞)
修复优先级:🔥 紧急