diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index 115b425..75d5ed8 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -171,11 +171,15 @@ export async function uploadContractTemplate( const uploadUrl = `${UPLOAD_URL}/upload_contract_template`; console.log('【合同模板上传】准备发送请求到服务器:', uploadUrl); - // 设置请求头(按需去掉JWT认证) + // 设置请求头 const headers: HeadersInit = { 'Accept': 'application/json' }; + if (jwtToken) { + headers['Authorization'] = `Bearer ${jwtToken}`; + } + // 发送请求 const response = await fetch(uploadUrl, { method: 'POST', @@ -251,11 +255,15 @@ export async function appendContractAttachments( const uploadUrl = `${UPLOAD_URL}/contracts/${documentId}/append_attachments`; console.log('【合同附件追加】准备发送请求到服务器:', uploadUrl); - // 设置请求头(根据需求,附件追加不做JWT认证) + // 设置请求头 const headers: HeadersInit = { 'Accept': 'application/json' }; + if (jwtToken) { + headers['Authorization'] = `Bearer ${jwtToken}`; + } + // 发送请求 const response = await fetch(uploadUrl, { method: 'POST', @@ -349,16 +357,12 @@ export async function uploadDocumentToServer( // const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, { try { // console.log('【调试】开始fetch请求...'); - const headers: HeadersInit = { - 'X-File-Name': encodeURIComponent(fileName), - 'Accept': 'application/json' - }; - if (jwtToken) { - headers['Authorization'] = `Bearer ${jwtToken}`; - } const response = await fetch(uploadUrl, { method: 'POST', - headers, + headers: { + 'X-File-Name': encodeURIComponent(fileName), + 'Authorization': `Bearer ${jwtToken || ''}` + }, body: formData }); diff --git a/app/api/login/auth.server.ts b/app/api/login/auth.server.ts index 603a652..6ba7a6f 100644 --- a/app/api/login/auth.server.ts +++ b/app/api/login/auth.server.ts @@ -736,18 +736,6 @@ 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', diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 965f722..66e18ca 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -340,10 +340,7 @@ const getCurrentConfig = (): ApiConfig => { return getConfigFromEnv(defaultConfig); } - // 调试信息(仅在开发环境显示) - if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { - console.log('🔧 最终配置:', defaultConfig); - } + console.log('🔧 最终配置:', defaultConfig); return defaultConfig; }; diff --git a/app/middleware/host-validation.ts b/app/middleware/host-validation.ts deleted file mode 100644 index 6d0f242..0000000 --- a/app/middleware/host-validation.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * 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: 这里可以添加更复杂的日志记录逻辑 - // 比如写入数据库、发送告警邮件等 -} diff --git a/app/root.tsx b/app/root.tsx index 1c3b379..69f81fd 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -38,11 +38,6 @@ import { logout, type UserRole } from "~/api/login/auth.server"; -import { - validateRequest, - isProtectedRoute, - logSecurityEvent -} from "~/middleware/host-validation"; // 定义需要高级权限的路径 export const developerOnlyPaths = [ @@ -75,32 +70,6 @@ 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)); diff --git a/app/routes/api.oauth.token.tsx b/app/routes/api.oauth.token.tsx index 95f5a8b..50e540d 100644 --- a/app/routes/api.oauth.token.tsx +++ b/app/routes/api.oauth.token.tsx @@ -1,7 +1,6 @@ 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的服务器端代理。 @@ -9,15 +8,7 @@ import { validateRequest, logSecurityEvent } from "~/middleware/host-validation" * 以避免在网络策略限制服务器直接访问外部服务时出现问题。 */ 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 Token API Host验证失败:', hostValidation.error); - return json({ success: false, error: "Forbidden: Invalid Host header" }, { status: 403 }); - } - - // 2. 只允许POST请求 + // 1. 只允许POST请求 if (request.method !== "POST") { return json({ success: false, error: "Method Not Allowed" }, { status: 405 }); } diff --git a/app/routes/api.oauth.userinfo.tsx b/app/routes/api.oauth.userinfo.tsx index beec8d5..a2c81ec 100644 --- a/app/routes/api.oauth.userinfo.tsx +++ b/app/routes/api.oauth.userinfo.tsx @@ -1,22 +1,12 @@ 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 }); } diff --git a/app/routes/callback.tsx b/app/routes/callback.tsx index 165d1ee..ca842ae 100644 --- a/app/routes/callback.tsx +++ b/app/routes/callback.tsx @@ -1,23 +1,8 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/node"; -import { createUserSession, saveUserInfo, type UserRole } from "~/api/login/auth.server"; +import { createUserSession, saveUserInfo } 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"); @@ -158,7 +143,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 使用统一的session创建函数 return createUserSession({ isAuthenticated: true, - userRole: userRole as UserRole, + userRole: userRole as 'common' | 'developer', redirectTo, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token, diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 41a5f06..2ecf9ca 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -788,14 +788,13 @@ export default function DocumentsIndex() { try { setAttachmentUploading(true); - const jwtToken = (loaderData.frontendJWT as string | undefined) || (loaderData.userInfo?.frontend_jwt as unknown as string | undefined); const result = await appendContractAttachments( selectedDocumentId, attachmentFiles, attachmentMergeMode, true, // isReprocess attachmentRemark || undefined, - jwtToken + loaderData.frontendJWT as string | undefined ); if (result.error) { @@ -863,12 +862,11 @@ export default function DocumentsIndex() { try { setTemplateUploading(true); - const jwtToken = (loaderData.frontendJWT as string | undefined) || (loaderData.userInfo?.frontend_jwt as unknown as string | undefined); const result = await uploadContractTemplate( templateFile, selectedDocumentId, undefined, // comparisonId - jwtToken + loaderData.frontendJWT as string | undefined ); if (result.error) { diff --git a/app/routes/rules-files.tsx b/app/routes/rules-files.tsx index 8285ada..060ffd3 100644 --- a/app/routes/rules-files.tsx +++ b/app/routes/rules-files.tsx @@ -512,14 +512,13 @@ export default function RulesFiles() { try { setAttachmentUploading(true); const docId = parseInt(selectedDocumentId, 10); - const jwtToken = (frontendJWT as string | undefined) || (userInfo?.frontend_jwt as unknown as string | undefined); const result = await appendContractAttachments( docId, attachmentFiles, attachmentMergeMode, true, attachmentRemark || undefined, - jwtToken + frontendJWT as string | undefined ); if (result.error) { throw new Error(result.error); @@ -666,7 +665,7 @@ export default function RulesFiles() { > 查看 - {reviewType === 'contract' && file.status === 'Processed' && ( + {file.fileTypeId === 1 && file.status === 'Processed' && (