feat: 1. 修改完善全局路由检测。 2. 完善统一的token认证管理,token失效自动跳转到登录页。

This commit is contained in:
2025-11-18 20:32:43 +08:00
parent e7b1c2e294
commit adfb84a31d
17 changed files with 270 additions and 294 deletions
+75 -134
View File
@@ -18,7 +18,6 @@
*/
import { createCookieSessionStorage } from "@remix-run/node";
import { tokenManager } from "./token-manager.server";
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
@@ -49,6 +48,8 @@ export interface UserInfo {
is_leader?: boolean; // 是否为部门负责人
area?: string; // 用户所属地区
id?: string | number; // 临时的用户id
user_id?: string | number;
user_role?: string
}
/**
@@ -198,150 +199,80 @@ export async function getUserSession(request: Request) {
const userInfo = session.get("userInfo");
let frontendJWT = session.get("frontendJWT");
let isTokenExpired = false;
let refreshedSession = null;
let shouldRegenerateJWT = false;
// 🔑 admin 用户不需要刷新 OAuth token,只需要维护 JWT
const isAdmin = userRole === 'admin';
// 如果有token信息,检查是否需要刷新(admin用户跳过OAuth token刷新)
if (!isAdmin && accessToken && refreshToken && tokenIssuedAt && tokenExpiresIn) {
try {
const tokenInfo = {
accessToken,
refreshToken,
tokenIssuedAt,
tokenExpiresIn
};
// 检查并自动刷新token
const refreshResult = await tokenManager.checkAndRefreshToken(tokenInfo);
if (refreshResult.success && refreshResult.newTokenInfo) {
const newToken = refreshResult.newTokenInfo;
// 如果token被刷新了,更新session
if (newToken.accessToken !== accessToken) {
console.log("Token已刷新,更新session");
session.set("accessToken", newToken.accessToken);
session.set("refreshToken", newToken.refreshToken);
session.set("tokenIssuedAt", newToken.tokenIssuedAt);
session.set("tokenExpiresIn", newToken.tokenExpiresIn);
// 更新本地变量
accessToken = newToken.accessToken;
tokenIssuedAt = newToken.tokenIssuedAt;
tokenExpiresIn = newToken.tokenExpiresIn;
// 标记需要重新生成JWT
shouldRegenerateJWT = true;
refreshedSession = session;
}
isTokenExpired = false;
} else {
console.error("Token刷新失败:", refreshResult.error);
isTokenExpired = true;
}
} catch (error) {
console.error("Token验证过程中出错:", error);
isTokenExpired = true;
}
} else if (isAdmin) {
// admin 用户:不检查 OAuth token 过期,始终保持登录状态
// console.log("admin 用户登录,跳过 OAuth token 刷新");
isTokenExpired = false;
// admin 用户需要有一个合理的 tokenExpiresIn 用于 JWT 生成
// 如果没有设置,使用一个默认值(如2小时)
if (!tokenExpiresIn) {
tokenExpiresIn = 7200; // 2小时
session.set("tokenExpiresIn", tokenExpiresIn);
refreshedSession = session;
}
// 🔑 新的统一过期检查逻辑
// 不区分 admin 和 OAuth 用户,所有用户都使用同样的过期检查
// 如果没有设置 tokenExpiresIn,给一个默认值(24小时)
if (!tokenExpiresIn) {
tokenExpiresIn = 86400; // 24小时(与后端登录接口返回的一致)
session.set("tokenExpiresIn", tokenExpiresIn);
refreshedSession = session;
}
// 检查前端JWT状态
if (isAuthenticated && !isTokenExpired && userInfo) {
let needsJWTRefresh = false;
// 检查是否有前端JWT
// 🔑 简化:不再自动重新生成 JWT
// JWT 即将过期时,直接让用户重新登录
// 🔑 关键:验证 session 完整性
// 如果 isAuthenticated 为 true,但缺少关键数据(userInfo),则认为 session 无效
let finalIsAuthenticated = isAuthenticated;
if (finalIsAuthenticated) {
// 检查是否有用户信息
if (!userInfo) {
console.warn("⚠️ [getUserSession] Session 不完整:缺少 userInfo");
finalIsAuthenticated = false;
}
// 检查是否有前端 JWT
if (!frontendJWT) {
needsJWTRefresh = true;
console.log("缺少前端JWT,需要生成");
} else {
// 检查JWT是否即将过期
if (JWTUtils.isJWTExpiringSoon(frontendJWT)) {
needsJWTRefresh = true;
console.log("前端JWT即将过期,需要重新生成");
}
console.warn("⚠️ [getUserSession] Session 不完整:缺少 frontendJWT");
finalIsAuthenticated = false;
}
// 如果OAuth token被刷新了,也需要重新生成JWT
if (shouldRegenerateJWT) {
needsJWTRefresh = true;
console.log("OAuth token已刷新,需要重新生成JWT");
}
// 重新生成JWT
if (needsJWTRefresh && tokenExpiresIn) {
try {
// 从userInfo中获取用户数据
if (userInfo.user_id && userInfo.sub) {
const mockSavedUserData: SsoUser = {
id: userInfo.user_id,
sub: userInfo.sub,
username: userInfo.username || userInfo.sub,
nick_name: userInfo.nick_name || "未知用户",
phone_number: userInfo.phone_number,
email: userInfo.email,
ou_id: userInfo.ou_id || "default",
ou_name: userInfo.ou_name || "未知部门",
status: 0,
is_leader: userInfo.is_leader || false
};
const newJWT = await generateFrontendJWT(userInfo, mockSavedUserData, userRole, tokenExpiresIn);
// 打印JWT重新生成信息
console.log("=== Token刷新时重新生成JWT ===");
// console.log("原始userInfo:", userInfo);
// console.log("重构的用户数据:", mockSavedUserData);
// console.log("用户角色:", userRole);
// console.log("新生成的JWT:", newJWT);
// console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
// 更新session中的JWT
if (!refreshedSession) {
refreshedSession = session;
}
refreshedSession.set("frontendJWT", newJWT);
// 更新userInfo以包含新的JWT
const updatedUserInfo = createUserInfoWithJWT(userInfo, mockSavedUserData, userRole, newJWT);
refreshedSession.set("userInfo", updatedUserInfo);
console.log("更新后的userInfo:", updatedUserInfo);
console.log("=== JWT重新生成完成 ===");
frontendJWT = newJWT;
}
} catch (error) {
console.error("生成前端JWT失败:", error);
// 🔑 统一的 token 过期检查(所有用户类型)
// 使用签发时间 + 有效期 - 5分钟缓冲来判断是否需要重新登录
if (tokenIssuedAt && tokenExpiresIn) {
const now = Date.now(); // 当前时间(毫秒)
const expiresAt = tokenIssuedAt + (tokenExpiresIn * 1000); // 过期时间(毫秒)
const bufferTime = 5 * 60 * 1000; // 5分钟缓冲时间(毫秒)
// 如果当前时间 >= 过期时间 - 5分钟,则认为 token 即将过期或已过期
if (now >= expiresAt - bufferTime) {
const remainingSeconds = Math.floor((expiresAt - now) / 1000);
console.error(`❌ [getUserSession] Token 即将过期或已过期 (剩余: ${remainingSeconds} 秒)`);
finalIsAuthenticated = false;
}
} else if (!tokenIssuedAt) {
// 如果没有签发时间,认为 session 无效(可能是旧版本数据)
console.warn("⚠️ [getUserSession] Session 缺少 tokenIssuedAt,认为已过期");
finalIsAuthenticated = false;
}
}
// 🚨 如果 session 无效(包括 token 过期),自动重定向到登录页
if (!finalIsAuthenticated && isAuthenticated) {
console.error("❌ [getUserSession] Session 已失效,清除 session 并重定向到登录页");
// 销毁服务端 session
const { redirect } = await import("@remix-run/node");
const destroyedSession = await sessionStorage.destroySession(session);
// 重定向到登录页,添加 expired=true 参数标识是因为过期重定向
throw redirect("/login?expired=true", {
headers: {
"Set-Cookie": destroyedSession
}
});
}
return {
isAuthenticated: isAuthenticated && !isTokenExpired,
isAuthenticated: finalIsAuthenticated,
userRole,
accessToken,
refreshToken,
userInfo,
isTokenExpired,
refreshedSession, // 如果刷新了token,返回更新后的session
frontendJWT // 返回前端JWT
};
@@ -370,27 +301,37 @@ export async function createUserSession(params: {
accessToken?: string;
refreshToken?: string;
tokenExpiresIn?: number;
tokenIssuedAt?: number; // 🔑 新增:支持传递签发时间
userInfo?: UserInfo;
frontendJWT?: string;
}) {
const session = await sessionStorage.getSession();
// 基础认证信息
session.set("isAuthenticated", params.isAuthenticated);
session.set("userRole", params.userRole);
// OAuth token信息
if (params.accessToken) {
session.set("accessToken", params.accessToken);
session.set("tokenIssuedAt", Date.now());
}
if (params.refreshToken) {
session.set("refreshToken", params.refreshToken);
}
// 🔑 Token 时间信息(与 accessToken 独立,支持管理员登录)
// 只要有 tokenExpiresIn 或 tokenIssuedAt,就设置这些字段
if (params.tokenExpiresIn) {
session.set("tokenExpiresIn", params.tokenExpiresIn);
}
if (params.tokenIssuedAt !== undefined) {
// 优先使用传递的 tokenIssuedAt
session.set("tokenIssuedAt", params.tokenIssuedAt);
} else if (params.accessToken || params.frontendJWT) {
// 如果没有传递 tokenIssuedAt,但有 tokenaccessToken 或 frontendJWT),则使用当前时间
session.set("tokenIssuedAt", Date.now());
}
// 用户信息和JWT
if (params.userInfo) {
session.set("userInfo", params.userInfo);