all in
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user