Files
leaudit-platform-frontend/app/routes/callback.tsx
T

185 lines
6.8 KiB
TypeScript

import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
import { createUserSession, saveUserInfo, type UserRole } from "~/api/login/auth.server";
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
import { validateRequest, logSecurityEvent } from "~/middleware/host-validation";
export async function loader({ request }: LoaderFunctionArgs) {
// ==================== Host头验证 ====================
// OAuth回调是安全敏感的操作,需要严格验证请求来源
const hostValidation = validateRequest(request);
if (!hostValidation.valid) {
// 记录安全事件
logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request);
console.error('❌ OAuth回调Host验证失败:', hostValidation.error);
return redirect("/login?error=invalid_host");
}
// console.log('✅ OAuth回调Host验证通过');
// ==================== Host头验证结束 ====================
const url = new URL(request.url);
const origin = url.origin; // 获取请求的源 (e.g., "http://10.79.97.17:51703")
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const error = url.searchParams.get("error");
const error_description = url.searchParams.get("error_description");
console.log("🔧 OAuth2.0回调参数:", {
code: code ? `${code.substring(0, 10)}...` : null,
state: state,
error: error,
error_description: error_description,
fullUrl: request.url
});
// 检查是否有错误
if (error) {
console.error("❌ OAuth2.0授权失败:", error, error_description);
return redirect(`/login?error=${encodeURIComponent(error_description || error)}`);
}
// 检查是否有授权码
if (!code) {
console.error("❌ OAuth2.0回调缺少授权码");
return redirect("/login?error=missing_code");
}
// 验证状态值
if (!state || !state.endsWith("_idp")) {
console.error("❌ OAuth2.0状态值验证失败:", { state, expectedSuffix: "_idp" });
return redirect("/login?error=invalid_state");
}
console.log("✅ OAuth2.0回调参数验证通过");
try {
console.log("🔧 开始处理OAuth2.0回调");
// --- 修改开始: 不再直接调用OAuthClient,而是通过内部代理API ---
// 获取访问令牌 (通过代理)
console.log(`🔧 [Callback] 开始通过内部代理获取访问令牌... (目标: ${origin}/api/oauth/token)`);
const proxyResponse = await fetch(`${origin}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
});
const tokenResponse = await proxyResponse.json();
if (!proxyResponse.ok || !tokenResponse.success) {
console.error("❌ [Callback] 通过内部代理获取访问令牌失败:", tokenResponse);
return redirect("/login?error=token_proxy_error");
}
// --- 修改结束 ---
console.log("✅ [Callback] 访问令牌获取成功");
// --- 修改开始: 通过内部代理获取用户信息 ---
console.log(`🔧 [Callback] 开始通过内部代理获取用户信息... (目标: ${origin}/api/oauth/userinfo)`);
const userInfoProxyResponse = await fetch(`${origin}/api/oauth/userinfo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ accessToken: tokenResponse.access_token }),
});
const userInfoResponse = await userInfoProxyResponse.json();
if (!userInfoProxyResponse.ok || !userInfoResponse.success) {
console.error("❌ [Callback] 通过内部代理获取用户信息失败:", userInfoResponse);
return redirect("/login?error=userinfo_proxy_error");
}
// 将代理返回的用户信息包装成与原有一致的结构
const userInfo = {
success: true,
data: userInfoResponse.data,
};
// --- 修改结束 ---
console.log("✅ [Callback] 用户信息获取成功");
// TODO 根据用户信息判断用户角色,这里可以根据实际业务逻辑调整 暂定都是common
const userRole = "common";
// 获取重定向URL
const redirectTo = url.searchParams.get("redirect") || "/";
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
const saveResult = await saveUserInfo(userInfo.data);
if (!saveResult.success) {
console.error("保存用户信息到数据库失败:", saveResult.error);
// 注意:即使保存到数据库失败,我们仍然继续登录流程,因为用户已经通过了身份验证
return redirect("/login?error=save_user_error");
}
console.log("用户信息已成功保存到数据库");
const savedUserData = saveResult.data!;
// 生成前端专用JWT
const jwtUserInfo: UserInfoForJWT = {
sub: userInfo.data.sub,
user_id: savedUserData.id!,
username: savedUserData.username,
nick_name: savedUserData.nick_name,
email: savedUserData.email,
phone_number: savedUserData.phone_number,
ou_id: savedUserData.ou_id,
ou_name: savedUserData.ou_name,
is_leader: savedUserData.is_leader,
user_role: userRole as 'common' | 'developer'
};
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
console.log("前端JWT已生成");
// 更新userInfo以包含数据库ID、JWT,并用数据库标准字段覆盖关键属性,确保 nick_name 等存在
const enhancedUserInfo = {
...userInfo.data, // 保留OAuth返回的原字段(包含 nickname 等)
username: savedUserData.username,
nick_name: savedUserData.nick_name,
phone_number: savedUserData.phone_number,
email: savedUserData.email,
ou_id: savedUserData.ou_id,
ou_name: savedUserData.ou_name,
status: savedUserData.status,
is_leader: savedUserData.is_leader,
user_id: savedUserData.id,
user_role: userRole,
frontend_jwt: frontendJWT
};
// 使用统一的session创建函数
return createUserSession({
isAuthenticated: true,
userRole: userRole as UserRole,
redirectTo,
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
tokenExpiresIn: tokenResponse.expires_in,
userInfo: enhancedUserInfo,
frontendJWT
});
} catch (error) {
console.error("OAuth2.0回调处理失败:", error);
return redirect("/login?error=callback_error");
}
}
export default function Callback() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-600">...</p>
</div>
</div>
);
}