peixue-dev/Archive/[一次性]消息通知权限问题修复说明.md

8.8 KiB
Raw Permalink Blame History

消息通知权限问题修复说明

🚨 严重问题

问题描述

所有用户(陪伴员、管理师、家长等)都能看到相同的消息列表,而不是只看到自己的消息。

从截图可以看到:

  • 显示了8335条未读消息
  • 所有消息都是"服务提醒"
  • 不同用户登录后看到的消息列表完全相同

问题根源

1. JWT拦截器配置错误

文件: peidu/backend/src/main/java/com/peidu/config/WebMvcConfig.java

问题代码第47行

registry.addInterceptor(jwtInterceptor)
    .addPathPatterns("/api/**")
    .excludePathPatterns(
        // ... 其他排除路径
        "/api/notification/**",  // ⚠️ 消息通知接口被排除在JWT拦截器之外
        // ...
    );

后果:

  1. 消息通知接口不需要登录就能访问(安全漏洞)
  2. JWT拦截器不会解析token
  3. request中没有userId属性
  4. 所有用户都无法获取到自己的userId

2. 默认返回userId=1

文件: peidu/backend/src/main/java/com/peidu/controller/NotificationController.java

问题代码第33-40行

private Long getCurrentUserId(HttpServletRequest request) {
    Object userId = request.getAttribute("userId");
    if (userId != null) {
        return Long.valueOf(userId.toString());
    }
    // 默认返回测试用户ID
    return 1L;  // ⚠️ 所有用户都返回userId=1
}

后果:

  1. 当无法获取userId时默认返回1L
  2. 所有用户查询的都是userId=1的消息
  3. 导致所有用户看到相同的消息列表

数据流程分析

用户登录 → 获取token → 前端调用 /api/notification/list
                              ↓
                    ❌ JWT拦截器被跳过因为路径被排除
                              ↓
                    NotificationController.getList()
                              ↓
                    getCurrentUserId() → request.getAttribute("userId") = null
                              ↓
                    ❌ 返回默认值 userId = 1L
                              ↓
                    查询 notification 表 WHERE user_id = 1
                              ↓
                    ❌ 所有用户都看到userId=1的消息

修复方案

修复1移除notification接口的JWT拦截器排除

文件: peidu/backend/src/main/java/com/peidu/config/WebMvcConfig.java

修改前:

.excludePathPatterns(
    // ...
    "/api/notification/**",  // ⚠️ 错误:排除了消息通知接口
    // ...
);

修改后:

.excludePathPatterns(
    // ...
    // "/api/notification/**",  // ⚠️ 已移除:消息通知需要登录才能访问
    // ...
);

修复2getCurrentUserId抛出异常而不是返回默认值

文件: peidu/backend/src/main/java/com/peidu/controller/NotificationController.java

修改前:

private Long getCurrentUserId(HttpServletRequest request) {
    Object userId = request.getAttribute("userId");
    if (userId != null) {
        return Long.valueOf(userId.toString());
    }
    // 默认返回测试用户ID
    return 1L;  // ⚠️ 错误:返回默认值
}

修改后:

private Long getCurrentUserId(HttpServletRequest request) {
    Object userId = request.getAttribute("userId");
    if (userId != null) {
        return Long.valueOf(userId.toString());
    }
    // ⚠️ 不应该返回默认值,应该抛出异常
    throw new RuntimeException("无法获取当前用户ID请先登录");
}

🔄 修复后的数据流程

用户登录 → 获取token → 前端调用 /api/notification/list
                              ↓
                    ✅ JWT拦截器执行
                              ↓
                    解析token → 获取userId → request.setAttribute("userId", userId)
                              ↓
                    NotificationController.getList()
                              ↓
                    getCurrentUserId() → request.getAttribute("userId") = 实际的userId
                              ↓
                    ✅ 返回当前登录用户的userId
                              ↓
                    查询 notification 表 WHERE user_id = 当前用户的userId
                              ↓
                    ✅ 每个用户只看到自己的消息

📝 测试步骤

步骤1重启后端服务

修改了拦截器配置,需要重启后端服务才能生效。

步骤2清除前端缓存

cd peidu/uniapp
npm run dev:mp-weixin

步骤3测试不同用户

测试陪伴员A

  1. 使用陪伴员A的账号登录
  2. 进入"消息通知"页面
  3. 查看消息列表
  4. 记录消息数量和内容

测试陪伴员B

  1. 使用陪伴员B的账号登录
  2. 进入"消息通知"页面
  3. 查看消息列表
  4. 记录消息数量和内容

测试管理师

  1. 使用管理师账号登录
  2. 进入"消息通知"页面
  3. 查看消息列表
  4. 记录消息数量和内容

步骤4验证结果

预期结果:

  • 不同用户看到的消息列表不同
  • 每个用户只看到发送给自己的消息
  • 未读数量显示正确
  • 消息内容显示正确

如果出现"未登录"错误:

  • 检查前端是否正确发送了token
  • 检查token是否过期
  • 重新登录获取新token

🔍 数据验证SQL

运行以下SQL检查数据库中的消息分布

-- 1. 查看各用户的消息数量分布
SELECT 
    user_id,
    COUNT(*) as total_count,
    SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread_count,
    MAX(create_time) as last_notification_time
FROM notification
WHERE deleted = 0
GROUP BY user_id
ORDER BY user_id;

-- 2. 查看userId=1的消息之前所有用户都看到的
SELECT 
    id,
    user_id,
    title,
    content,
    type,
    is_read,
    create_time
FROM notification
WHERE user_id = 1 AND deleted = 0
ORDER BY create_time DESC
LIMIT 20;

-- 3. 查看最近创建的消息
SELECT 
    id,
    user_id,
    title,
    content,
    type,
    is_read,
    create_time
FROM notification
WHERE deleted = 0
ORDER BY create_time DESC
LIMIT 50;

-- 4. 统计各类型消息的数量
SELECT 
    type,
    COUNT(*) as count,
    SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread_count
FROM notification
WHERE deleted = 0
GROUP BY type
ORDER BY count DESC;

⚠️ 其他需要检查的控制器

以下控制器也使用了类似的getCurrentUserId()方法,需要检查是否有相同的问题:

  1. CheckInRecordController - 签到记录
  2. ManagerApplicationController - 管理师申请
  3. StudentController - 学生管理
  4. TeacherController - 陪伴员管理
  5. WithdrawController - 提现管理
  6. CalendarController - 日历管理

检查要点:

  • 这些接口是否被排除在JWT拦截器之外
  • getCurrentUserId()是否返回默认值?
  • 是否存在类似的权限问题?

🎯 安全建议

1. 统一的用户身份获取方式

建议创建一个基础控制器类:

public abstract class BaseController {
    
    protected Long getCurrentUserId(HttpServletRequest request) {
        Object userId = request.getAttribute("userId");
        if (userId == null) {
            throw new BusinessException(401, "未登录或登录已过期");
        }
        return Long.valueOf(userId.toString());
    }
    
    protected Long getCurrentUserIdOrNull(HttpServletRequest request) {
        Object userId = request.getAttribute("userId");
        return userId != null ? Long.valueOf(userId.toString()) : null;
    }
}

2. 最小权限原则

  • 默认所有接口都需要登录
  • 只有明确的公开接口才排除JWT拦截器
  • 定期审查excludePathPatterns配置

3. 添加日志记录

在关键操作中记录userId

log.info("用户{}查询消息列表,页码:{},类型:{}", userId, page, type);

4. 添加单元测试

测试不同用户只能看到自己的消息:

@Test
public void testUserCanOnlySeeOwnNotifications() {
    // 测试用户A只能看到自己的消息
    // 测试用户B只能看到自己的消息
    // 测试用户A看不到用户B的消息
}

📊 修改文件清单

文件 修改内容 状态
peidu/backend/src/main/java/com/peidu/config/WebMvcConfig.java 移除notification接口的JWT拦截器排除 已完成
peidu/backend/src/main/java/com/peidu/controller/NotificationController.java getCurrentUserId抛出异常而不是返回默认值 已完成
Archive/[一次性]消息通知权限问题修复说明.md 创建修复说明文档 已完成

修复完成时间: 2026-01-26

修复人员: Kiro AI

严重程度: 🚨 高危(安全漏洞)

测试状态: ⚠️ 需要重启后端服务并测试