Merge remote-tracking branch 'origin/shiy-login' into shiy-login

This commit is contained in:
2025-09-22 11:19:18 +08:00
7 changed files with 322 additions and 4 deletions
+12
View File
@@ -736,6 +736,18 @@ export async function simpleRootLogin(
});
}
// 密码强度验证
// const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
// if (!passwordRegex.test(password.trim())) {
// return new Response(JSON.stringify({
// success: false,
// error: "密码必须至少8位,包含大小写字母和数字"
// }), {
// status: 400,
// headers: { "Content-Type": "application/json" }
// });
// }
// 调用登录接口
const loginResponse = await fetch(`${API_BASE_URL}/password_login`, {
method: 'POST',
+3
View File
@@ -340,7 +340,10 @@ const getCurrentConfig = (): ApiConfig => {
return getConfigFromEnv(defaultConfig);
}
// 调试信息(仅在开发环境显示)
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
console.log('🔧 最终配置:', defaultConfig);
}
return defaultConfig;
};
+238
View File
@@ -0,0 +1,238 @@
/**
* Host头验证中间件
* 用于防止Host Header注入攻击
*/
interface HostValidationResult {
valid: boolean;
error?: string;
allowedHost?: string;
}
/**
* 获取允许的Host列表
* 根据不同环境和端口配置返回相应的Host白名单
*/
function getAllowedHosts(): string[] {
// 基础IP地址
const baseIP = '10.79.97.17';
const testIP = '172.16.0.55';
const localhostIP = '127.0.0.1';
// 生产环境端口列表
const productionPorts = ['51703', '51704', '51705', '51706', '51707', '51708'];
// 测试环境端口列表
const testPorts = ['5173', '5174', '5175', '5176', '5177', '5178'];
const allowedHosts: string[] = [];
// 添加基础IP(不带端口)
allowedHosts.push(baseIP, testIP, 'localhost', localhostIP);
// 添加生产环境的IP:端口组合
productionPorts.forEach(port => {
allowedHosts.push(`${baseIP}:${port}`);
});
// 添加测试环境的IP:端口组合
testPorts.forEach(port => {
allowedHosts.push(`${testIP}:${port}`);
allowedHosts.push(`localhost:${port}`);
allowedHosts.push(`${localhostIP}:${port}`);
});
// 开发环境额外允许的Host
if (process.env.NODE_ENV === 'development') {
allowedHosts.push('localhost:3000', '127.0.0.1:3000');
}
return allowedHosts;
}
/**
* 验证Host头是否在允许列表中
* @param request - Remix Request对象
* @returns 验证结果
*/
export function validateHost(request: Request): HostValidationResult {
const host = request.headers.get('host');
const referer = request.headers.get('referer');
const userAgent = request.headers.get('user-agent');
// 获取允许的Host列表
const allowedHosts = getAllowedHosts();
console.log('🔒 Host验证开始:', {
host: host,
referer: referer,
userAgent: userAgent ? userAgent.substring(0, 50) + '...' : null,
allowedHosts: allowedHosts
});
// 1. 验证Host头是否存在
if (!host) {
console.error('❌ Host头缺失');
return {
valid: false,
error: 'Missing Host header'
};
}
// 2. 验证Host头是否在允许列表中
if (!allowedHosts.includes(host)) {
console.error('❌ Host头不在允许列表中:', {
host: host,
allowedHosts: allowedHosts
});
return {
valid: false,
error: `Invalid Host header: ${host}`
};
}
// 3. 验证Referer头(如果存在)
if (referer) {
try {
const refererUrl = new URL(referer);
const refererHost = refererUrl.host;
if (!allowedHosts.includes(refererHost)) {
console.error('❌ Referer头不在允许列表中:', {
referer: referer,
refererHost: refererHost,
allowedHosts: allowedHosts
});
return {
valid: false,
error: `Invalid Referer header: ${refererHost}`
};
}
} catch (error) {
console.error('❌ Referer头格式无效:', referer);
return {
valid: false,
error: `Malformed Referer header: ${referer}`
};
}
}
console.log('✅ Host验证通过:', host);
return {
valid: true,
allowedHost: host
};
}
/**
* 验证Origin头是否在允许列表中
* @param request - Remix Request对象
* @returns 验证结果
*/
export function validateOrigin(request: Request): HostValidationResult {
const origin = request.headers.get('origin');
if (!origin) {
// Origin头不是必须的,某些请求(如直接访问)可能没有Origin头
return { valid: true };
}
try {
const originUrl = new URL(origin);
const originHost = originUrl.host;
const allowedHosts = getAllowedHosts();
if (!allowedHosts.includes(originHost)) {
console.error('❌ Origin头不在允许列表中:', {
origin: origin,
originHost: originHost,
allowedHosts: allowedHosts
});
return {
valid: false,
error: `Invalid Origin header: ${originHost}`
};
}
console.log('✅ Origin验证通过:', origin);
return { valid: true };
} catch (error) {
console.error('❌ Origin头格式无效:', origin);
return {
valid: false,
error: `Malformed Origin header: ${origin}`
};
}
}
/**
* 完整的请求验证
* 包括Host、Referer、Origin头的验证
* @param request - Remix Request对象
* @returns 验证结果
*/
export function validateRequest(request: Request): HostValidationResult {
// 1. 验证Host头
const hostValidation = validateHost(request);
if (!hostValidation.valid) {
return hostValidation;
}
// 2. 验证Origin头
const originValidation = validateOrigin(request);
if (!originValidation.valid) {
return originValidation;
}
return { valid: true };
}
/**
* 检查是否为受保护的路由
* 某些路由可能需要更严格的验证
* @param pathname - 请求路径
* @returns 是否为受保护路由
*/
export function isProtectedRoute(pathname: string): boolean {
const protectedRoutes = [
'/callback',
'/api/oauth/token',
'/api/oauth/userinfo',
'/logout',
'/admin'
];
return protectedRoutes.some(route => pathname.startsWith(route));
}
/**
* 记录安全事件
* @param event - 事件类型
* @param details - 事件详情
* @param request - 请求对象
*/
export function logSecurityEvent(
event: 'host_validation_failed' | 'origin_validation_failed' | 'referer_validation_failed',
details: string,
request: Request
) {
const timestamp = new Date().toISOString();
const url = new URL(request.url);
const clientIP = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown';
console.error(`🚨 安全事件: ${event}`, {
timestamp: timestamp,
url: url.pathname + url.search,
host: request.headers.get('host'),
referer: request.headers.get('referer'),
origin: request.headers.get('origin'),
userAgent: request.headers.get('user-agent'),
clientIP: clientIP,
details: details
});
// TODO: 这里可以添加更复杂的日志记录逻辑
// 比如写入数据库、发送告警邮件等
}
+31
View File
@@ -38,6 +38,11 @@ import {
logout,
type UserRole
} from "~/api/login/auth.server";
import {
validateRequest,
isProtectedRoute,
logSecurityEvent
} from "~/middleware/host-validation";
// 定义需要高级权限的路径
export const developerOnlyPaths = [
@@ -70,6 +75,32 @@ export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const pathname = url.pathname;
// ==================== Host头验证 ====================
// 1. 首先进行Host头验证,防止Host Header注入攻击
const hostValidation = validateRequest(request);
if (!hostValidation.valid) {
// 记录安全事件
logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request);
// 对于受保护的路由,直接返回403错误
if (isProtectedRoute(pathname)) {
throw new Response("Forbidden: Invalid Host header", {
status: 403,
statusText: "Forbidden"
});
}
// 对于普通路由,重定向到错误页面
console.error('❌ Host验证失败:', hostValidation.error);
throw new Response("Forbidden: Invalid request headers", {
status: 403,
statusText: "Forbidden"
});
}
// console.log('✅ Host验证通过,继续处理请求');
// ==================== Host头验证结束 ====================
// 排除不需要登录验证的路径
const publicPaths = ['/login', '/favicon.ico', '/callback'];
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
+10 -1
View File
@@ -1,6 +1,7 @@
import { type ActionFunctionArgs, json } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { OAUTH_CONFIG } from "~/config/api-config";
import { validateRequest, logSecurityEvent } from "~/middleware/host-validation";
/**
* 这个Action作为获取OAuth Access Token的服务器端代理。
@@ -8,7 +9,15 @@ import { OAUTH_CONFIG } from "~/config/api-config";
* 以避免在网络策略限制服务器直接访问外部服务时出现问题。
*/
export async function action({ request }: ActionFunctionArgs) {
// 1. 只允许POST请求
// 1. Host头验证
const hostValidation = validateRequest(request);
if (!hostValidation.valid) {
logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request);
console.error('❌ OAuth Token API Host验证失败:', hostValidation.error);
return json({ success: false, error: "Forbidden: Invalid Host header" }, { status: 403 });
}
// 2. 只允许POST请求
if (request.method !== "POST") {
return json({ success: false, error: "Method Not Allowed" }, { status: 405 });
}
+10
View File
@@ -1,12 +1,22 @@
import { type ActionFunctionArgs, json } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { OAUTH_CONFIG } from "~/config/api-config";
import { validateRequest, logSecurityEvent } from "~/middleware/host-validation";
/**
* 这个Action作为获取用户信息的服务器端代理。
* 它接收来自前端的`access_token`,然后在后端安全地获取用户信息。
*/
export async function action({ request }: ActionFunctionArgs) {
// 1. Host头验证
const hostValidation = validateRequest(request);
if (!hostValidation.valid) {
logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request);
console.error('❌ OAuth UserInfo API Host验证失败:', hostValidation.error);
return json({ success: false, error: "Forbidden: Invalid Host header" }, { status: 403 });
}
// 2. 只允许POST请求
if (request.method !== "POST") {
return json({ success: false, error: "Method Not Allowed" }, { status: 405 });
}
+17 -2
View File
@@ -1,8 +1,23 @@
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
import { createUserSession, saveUserInfo } from "~/api/login/auth.server";
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");
@@ -143,7 +158,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 使用统一的session创建函数
return createUserSession({
isAuthenticated: true,
userRole: userRole as 'common' | 'developer',
userRole: userRole as UserRole,
redirectTo,
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,