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

11 KiB
Raw Blame History

🔐 问卷权限问题分析报告

📋 问题描述

用户反馈:登录系统后能看到所有问卷,明明没有给用户分配问卷权限。

影响范围:所有普通用户都能看到系统中的所有问卷,存在严重的权限控制漏洞。

🔍 问题根源

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,添加完整的权限控制逻辑。

优点

  • 问卷和量表使用统一的权限机制
  • 安全性高,符合最小权限原则
  • 可以精确控制每个用户看到的问卷

实现步骤

  1. PsyQuestionnaireController 中注入 IPsyScalePermissionService
  2. 添加 @PreAuthorize 权限注解
  3. 实现权限过滤逻辑(参考 PsyScaleController
  4. 修改 /list 接口,根据用户权限返回问卷列表

方案二:问卷永久对所有人开放(不推荐)

如果业务上问卷确实需要对所有人开放(不需要权限控制),需要:

  1. 明确文档说明:问卷对所有登录用户开放
  2. 确保量表接口的权限过滤逻辑不影响问卷显示
  3. 前端统一使用量表接口(/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管理员

预期

  • 能看到所有问卷(不受权限限制)

测试步骤

  1. 准备数据

    • 创建3个问卷A未配置权限、B已配置权限、C已配置权限
    • 创建普通用户 user1只分配问卷B的权限
  2. 测试 user1 登录

    # 预期结果只能看到问卷A和B不能看到C
    GET /psychology/questionnaire/list
    
  3. 测试管理员登录

    # 预期结果能看到所有问卷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日
严重程度⚠️ 高危(权限控制漏洞)
修复优先级🔥 紧急