fix: bootstrap session after password login

This commit is contained in:
wren
2026-04-29 15:58:59 +08:00
parent 0369de68cc
commit 7dbeaac5f8
3 changed files with 502 additions and 520 deletions
+13 -9
View File
@@ -11,9 +11,11 @@ import { isAuthenticated } from '~/utils/auth-storage';
interface ClientAuthGuardProps { interface ClientAuthGuardProps {
isPublicPath: boolean; isPublicPath: boolean;
frontendJWT?: string;
userInfo?: Record<string, unknown>;
} }
export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) { export function ClientAuthGuard({ isPublicPath, frontendJWT, userInfo }: ClientAuthGuardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -29,15 +31,17 @@ export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
return; return;
} }
// 检查客户端是否已认证(localStorage 中有 token // 优先用服务端 session 回传的数据回填 localStorage,避免刚登录时客户端误判未登录
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
const authenticated = isAuthenticated(); if (!token && frontendJWT) {
localStorage.setItem('access_token', frontendJWT);
if (userInfo) {
localStorage.setItem('user_info', JSON.stringify(userInfo));
}
console.log('✅ [Auth Guard] 已根据服务端 session 回填本地认证数据');
}
// console.log('🔍 [Auth Guard] 认证检查', { const authenticated = isAuthenticated() || !!frontendJWT;
// token: token ? `${token.substring(0, 20)}...` : null,
// authenticated,
// pathname: location.pathname
// });
if (!authenticated) { if (!authenticated) {
console.log('🔒 [Auth Guard] 未认证,重定向到登录页'); console.log('🔒 [Auth Guard] 未认证,重定向到登录页');
@@ -50,7 +54,7 @@ export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
} else { } else {
// console.log('✅ [Auth Guard] 已认证,允许访问'); // console.log('✅ [Auth Guard] 已认证,允许访问');
} }
}, [isPublicPath, navigate, location.pathname]); }, [isPublicPath, navigate, location.pathname, frontendJWT, userInfo]);
// 这个组件不渲染任何内容 // 这个组件不渲染任何内容
return null; return null;
+5 -2
View File
@@ -205,6 +205,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
let userRole: UserRole = 'common'; // 默认为普通用户 let userRole: UserRole = 'common'; // 默认为普通用户
let userArea: string = ''; let userArea: string = '';
let frontendJWT: string | null = null; let frontendJWT: string | null = null;
let userInfo: any = null;
let allowedPaths: string[] = []; // 用户允许访问的路由列表 let allowedPaths: string[] = []; // 用户允许访问的路由列表
let permissionMap: Record<string, string[]> = {}; // ✅ 权限映射表 let permissionMap: Record<string, string[]> = {}; // ✅ 权限映射表
@@ -215,6 +216,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
userRole = session.userRole; userRole = session.userRole;
userArea = session.userInfo?.area || ''; userArea = session.userInfo?.area || '';
frontendJWT = session.frontendJWT || null; frontendJWT = session.frontendJWT || null;
userInfo = session.userInfo || null;
// 🔑 检查用户角色和JWT是否为空 // 🔑 检查用户角色和JWT是否为空
if (!userRole || userRole === '') { if (!userRole || userRole === '') {
@@ -352,6 +354,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
userArea, // ✅ 返回用户所属地区 userArea, // ✅ 返回用户所属地区
pathname, pathname,
frontendJWT, frontendJWT,
userInfo,
isPublicPath, // 传递给客户端,用于判断是否需要认证 isPublicPath, // 传递给客户端,用于判断是否需要认证
isMobile, // 🔒 传递移动端标识 isMobile, // 🔒 传递移动端标识
permissionMap, // ✅ 传递权限映射表 permissionMap, // ✅ 传递权限映射表
@@ -390,7 +393,7 @@ export function links() {
} }
export default function App() { export default function App() {
const { userRole, ENV, frontendJWT, isPublicPath, isMobile } = useLoaderData<typeof loader>(); const { userRole, ENV, frontendJWT, userInfo, isPublicPath, isMobile } = useLoaderData<typeof loader>();
return ( return (
@@ -424,7 +427,7 @@ export default function App() {
<MessageModalProvider> <MessageModalProvider>
<ToastProvider> <ToastProvider>
{/* 客户端认证守卫 - 在客户端检查 localStorage 中的 token */} {/* 客户端认证守卫 - 在客户端检查 localStorage 中的 token */}
<ClientAuthGuard isPublicPath={isPublicPath} /> <ClientAuthGuard isPublicPath={isPublicPath} frontendJWT={frontendJWT || undefined} userInfo={userInfo || undefined} />
{/* 🔑 公共路径(登录页、回调页)不显示 Layout,直接渲染内容 */} {/* 🔑 公共路径(登录页、回调页)不显示 Layout,直接渲染内容 */}
{isPublicPath ? ( {isPublicPath ? (
+4 -29
View File
@@ -152,38 +152,13 @@ export async function action({ request }: ActionFunctionArgs) {
console.log("👤 [Login Action] 用户角色:", user_info.user_role); // 应该是 "admin" console.log("👤 [Login Action] 用户角色:", user_info.user_role); // 应该是 "admin"
console.log("⏰ [Login Action] Token 有效期:", expires_in, "秒 (", expires_in / 3600, "小时)"); console.log("⏰ [Login Action] Token 有效期:", expires_in, "秒 (", expires_in / 3600, "小时)");
// 获取当前 URL 用于构建 callback URL // ✅ 账密登录直接写入 Cookie Session 并跳首页
const url = new URL(request.url); // localStorage 由 root 中的客户端会话引导逻辑补写,避免 callback 跳转链路卡住
// 🔑 重要:将 token 和用户信息作为 URL 参数传递给客户端
// 复用 OAuth 登录的 callback 页面逻辑
const callbackUrl = new URL('/callback', url.origin);
callbackUrl.searchParams.set('token', access_token);
callbackUrl.searchParams.set('userInfo', encodeURIComponent(JSON.stringify({
user_id: user_info.user_id,
username: user_info.username,
nick_name: user_info.nick_name,
email: user_info.email,
phone_number: user_info.phone_number,
ou_id: user_info.ou_id,
ou_name: user_info.ou_name,
is_leader: user_info.is_leader,
user_role: user_info.user_role,
area: user_info.area,
sub: user_info.sub,
// 🔑 包含后端返回的组织信息字段(可能为null)
tenant_name: user_info.tenant_name,
dep_name: user_info.dep_name,
dep_short_name: user_info.dep_short_name,
})));
callbackUrl.searchParams.set('redirectTo', redirectTo);
// ✅ 使用统一的 session 创建函数(和 OAuth 登录一样)
return createUserSession({ return createUserSession({
isAuthenticated: true, isAuthenticated: true,
userRole: user_info.user_role, userRole: user_info.user_role,
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token redirectTo,
frontendJWT: access_token, // 保存到 Cookie Session frontendJWT: access_token,
tokenExpiresIn: expires_in, tokenExpiresIn: expires_in,
tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间 tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间
userInfo: { userInfo: {