269 lines
6.8 KiB
Markdown
269 lines
6.8 KiB
Markdown
# Session 完整性验证 - 修复说明
|
||
|
||
## 🐛 问题描述
|
||
|
||
**现象**:用户访问页面时出现错误:
|
||
```
|
||
Uncaught TypeError: Cannot read properties of undefined (reading 'nick_name')
|
||
at Home (home.tsx:315:33)
|
||
```
|
||
|
||
**根本原因**:Session 部分过期或数据丢失时,`getUserSession` 仍然返回 `isAuthenticated: true`,但 `userInfo` 为 `undefined`。
|
||
|
||
## 🔍 问题分析
|
||
|
||
### 问题场景
|
||
|
||
```
|
||
用户登录成功
|
||
↓
|
||
Cookie Session 创建:
|
||
- isAuthenticated: true
|
||
- userInfo: { nick_name: "张三", ... }
|
||
- accessToken: "..."
|
||
- frontendJWT: "..."
|
||
↓
|
||
经过一段时间(Session 部分失效)
|
||
↓
|
||
Cookie Session 变为:
|
||
- isAuthenticated: true ✅ (仍然保留)
|
||
- userInfo: undefined ❌ (已丢失)
|
||
- accessToken: undefined ❌ (已丢失)
|
||
- frontendJWT: undefined ❌ (已丢失)
|
||
↓
|
||
getUserSession 返回:
|
||
- isAuthenticated: true ❌ (错误!应该是 false)
|
||
- userInfo: undefined
|
||
↓
|
||
home.tsx 访问 userInfo.nick_name
|
||
↓
|
||
💥 TypeError: Cannot read properties of undefined
|
||
```
|
||
|
||
### 原有代码问题
|
||
|
||
**`app/api/login/auth.server.ts:340-349` (修复前)**:
|
||
|
||
```typescript
|
||
return {
|
||
isAuthenticated: isAuthenticated && !isTokenExpired, // ❌ 只检查 token 过期
|
||
userRole,
|
||
accessToken,
|
||
refreshToken,
|
||
userInfo, // ❌ 可能是 undefined
|
||
isTokenExpired,
|
||
refreshedSession,
|
||
frontendJWT // ❌ 可能是 undefined
|
||
};
|
||
```
|
||
|
||
**问题**:
|
||
1. 只检查 `isTokenExpired`(需要满足特定条件才会检查)
|
||
2. 不验证 `userInfo` 是否存在
|
||
3. 不验证 `frontendJWT` 是否存在
|
||
4. 不验证 OAuth token 信息是否完整
|
||
|
||
## ✅ 解决方案
|
||
|
||
### 1. 修复 `getUserSession` - 添加完整性验证
|
||
|
||
**位置**:`app/api/login/auth.server.ts:340-374`
|
||
|
||
```typescript
|
||
// 🔑 关键:验证 session 完整性
|
||
// 如果 isAuthenticated 为 true,但缺少关键数据(userInfo),则认为 session 无效
|
||
let finalIsAuthenticated = isAuthenticated && !isTokenExpired;
|
||
|
||
if (finalIsAuthenticated) {
|
||
// 检查是否有用户信息
|
||
if (!userInfo) {
|
||
console.warn("⚠️ [getUserSession] Session 不完整:缺少 userInfo");
|
||
finalIsAuthenticated = false;
|
||
}
|
||
|
||
// 对于非 admin 用户,检查是否有必要的 OAuth token 信息
|
||
if (!isAdmin && (!accessToken || !refreshToken)) {
|
||
console.warn("⚠️ [getUserSession] Session 不完整:缺少 OAuth token 信息");
|
||
finalIsAuthenticated = false;
|
||
}
|
||
|
||
// 检查是否有前端 JWT
|
||
if (!frontendJWT) {
|
||
console.warn("⚠️ [getUserSession] Session 不完整:缺少 frontendJWT");
|
||
finalIsAuthenticated = false;
|
||
}
|
||
}
|
||
|
||
return {
|
||
isAuthenticated: finalIsAuthenticated, // ✅ 经过完整性验证
|
||
userRole,
|
||
accessToken,
|
||
refreshToken,
|
||
userInfo,
|
||
isTokenExpired,
|
||
refreshedSession,
|
||
frontendJWT
|
||
};
|
||
```
|
||
|
||
### 2. 增强 `home.tsx` loader - 服务端检查
|
||
|
||
**位置**:`app/routes/home.tsx:48-85`
|
||
|
||
```typescript
|
||
export async function loader({ request }: LoaderFunctionArgs) {
|
||
try {
|
||
const { userRole, userInfo, frontendJWT, isAuthenticated } = await getUserSession(request);
|
||
|
||
// 🔑 检查用户是否已登录且有用户信息
|
||
if (!isAuthenticated || !userInfo) {
|
||
console.warn("⚠️ [Home Loader] 用户未登录或缺少用户信息,重定向到登录页");
|
||
const url = new URL(request.url);
|
||
return Response.redirect(`/login?redirect=${encodeURIComponent(url.pathname)}`, 302);
|
||
}
|
||
|
||
return Response.json({
|
||
homeData: { /* ... */ },
|
||
userRole,
|
||
userInfo,
|
||
frontendJWT
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to fetch dashboard data:', error);
|
||
return Response.json({ error: '获取数据失败,请稍后重试' }, { status: 500 });
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 增强 Home 组件 - 客户端检查
|
||
|
||
**位置**:`app/routes/home.tsx:111-118`
|
||
|
||
```typescript
|
||
export default function Home() {
|
||
const { userInfo, frontendJWT, /* ... */ } = useLoaderData<typeof loader>();
|
||
|
||
// 🔑 防御性检查:如果 userInfo 不存在,重定向到登录页
|
||
if (!userInfo) {
|
||
console.error("❌ [Home] userInfo 不存在,重定向到登录页");
|
||
if (typeof window !== 'undefined') {
|
||
window.location.href = '/login';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ... 正常渲染
|
||
}
|
||
```
|
||
|
||
## 📊 完整数据流
|
||
|
||
### 修复后的流程
|
||
|
||
```
|
||
用户访问 /home
|
||
↓
|
||
服务端 loader 执行
|
||
↓
|
||
调用 getUserSession(request)
|
||
↓
|
||
检查 session 完整性:
|
||
- isAuthenticated: true
|
||
- userInfo: undefined ❌
|
||
- frontendJWT: undefined ❌
|
||
↓
|
||
完整性验证失败
|
||
↓
|
||
返回 { isAuthenticated: false, userInfo: undefined, ... }
|
||
↓
|
||
home.tsx loader 检查:
|
||
if (!isAuthenticated || !userInfo) { ... }
|
||
↓
|
||
返回 302 重定向
|
||
↓
|
||
浏览器跳转到 /login?redirect=/home
|
||
↓
|
||
✅ 用户看到登录页,而不是错误页面
|
||
```
|
||
|
||
## 🧪 测试场景
|
||
|
||
### 场景 1:Session 部分失效
|
||
|
||
```bash
|
||
# 模拟:手动清除 Cookie 中的部分数据(保留 isAuthenticated)
|
||
# 预期结果:重定向到登录页
|
||
```
|
||
|
||
### 场景 2:Session 完全有效
|
||
|
||
```bash
|
||
# 正常登录,访问 /home
|
||
# 预期结果:正常显示首页
|
||
```
|
||
|
||
### 场景 3:Session 完全失效
|
||
|
||
```bash
|
||
# 清除所有 Cookie
|
||
# 预期结果:重定向到登录页
|
||
```
|
||
|
||
## ⚙️ Session 完整性检查规则
|
||
|
||
### 必须存在的数据
|
||
|
||
对于所有已认证用户:
|
||
- ✅ `userInfo` - 用户信息对象
|
||
- ✅ `frontendJWT` - 前端 JWT token
|
||
|
||
对于 OAuth 登录用户(非 admin):
|
||
- ✅ `accessToken` - OAuth 访问令牌
|
||
- ✅ `refreshToken` - OAuth 刷新令牌
|
||
|
||
对于 admin 用户:
|
||
- ✅ `userInfo` - 用户信息对象
|
||
- ✅ `frontendJWT` - 前端 JWT token
|
||
- ⚠️ OAuth token 可选(使用模拟 token)
|
||
|
||
### 验证逻辑
|
||
|
||
```typescript
|
||
if (isAuthenticated) {
|
||
if (!userInfo) → isAuthenticated = false
|
||
if (!frontendJWT) → isAuthenticated = false
|
||
if (!isAdmin && !accessToken) → isAuthenticated = false
|
||
if (!isAdmin && !refreshToken) → isAuthenticated = false
|
||
}
|
||
```
|
||
|
||
## 🎯 修复收益
|
||
|
||
1. **用户体验改善**:Session 失效时看到登录页,而不是错误页面
|
||
2. **安全性增强**:防止部分 session 数据泄露导致的安全问题
|
||
3. **稳定性提升**:防止 `undefined` 访问导致的运行时错误
|
||
4. **调试友好**:清晰的日志输出,便于定位问题
|
||
|
||
## 📁 修改的文件
|
||
|
||
1. **`app/api/login/auth.server.ts`**
|
||
- 添加 session 完整性验证逻辑(line 340-374)
|
||
|
||
2. **`app/routes/home.tsx`**
|
||
- Loader 添加 `isAuthenticated` 和 `userInfo` 检查(line 54-58)
|
||
- 组件添加客户端防御性检查(line 112-118)
|
||
|
||
3. **`docs/Session完整性验证_修复说明.md`**(新增)
|
||
- 完整的问题分析和修复文档
|
||
|
||
## 🔗 相关文档
|
||
|
||
- `docs/服务端Token过期处理_实现说明.md` - Token 过期处理机制
|
||
- `docs/统一登录流程_实施记录.md` - 登录流程文档
|
||
- `docs/Token自动管理_测试说明.md` - Token 管理架构
|
||
|
||
---
|
||
|
||
**修复时间**: 2025-01-18
|
||
**修复者**: Claude Code
|