xinli/问卷权限问题分析报告.md
2025-12-02 15:12:55 +08:00

358 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔐 问卷权限问题分析报告
## 📋 问题描述
**用户反馈**:登录系统后能看到所有问卷,明明没有给用户分配问卷权限。
**影响范围**:所有普通用户都能看到系统中的所有问卷,存在严重的权限控制漏洞。
## 🔍 问题根源
### 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日
**严重程度**:⚠️ 高危(权限控制漏洞)
**修复优先级**:🔥 紧急