xinli/问卷权限问题分析报告.md

358 lines
11 KiB
Markdown
Raw Normal View History

2025-12-02 15:12:55 +08:00
# 🔐 问卷权限问题分析报告
## 📋 问题描述
**用户反馈**:登录系统后能看到所有问卷,明明没有给用户分配问卷权限。
**影响范围**:所有普通用户都能看到系统中的所有问卷,存在严重的权限控制漏洞。
## 🔍 问题根源
### 1. 问卷列表接口缺少权限控制
**文件**`ry-xinli-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQuestionnaireController.java`
```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`
```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` 中,问卷的处理逻辑是:
```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<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管理员
**预期**
- ✅ 能看到所有问卷(不受权限限制)
### 测试步骤
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日
**严重程度**:⚠️ 高危(权限控制漏洞)
**修复优先级**:🔥 紧急