/** * 票据认证工具 * 处理从门户传递过来的票据,获取访问令牌 */ // 票据处理接口(直接使用完整URL)测试环境 const TICKET_PROCESS_API = 'https://aqai.shudaodsj.com:22001/api/ticket/process' // ===== 关键修复:在模块加载时立即保存原始 URL ===== // 防止其他请求(如 axios 拦截器)在认证完成前跳转导致票据丢失 let originalUrl = null let originalSearch = null let originalHash = null // 立即保存原始 URL(在任何其他代码执行之前) try { originalUrl = window.location.href originalSearch = window.location.search originalHash = window.location.hash console.log('💾 已保存原始 URL:', originalUrl) console.log('💾 已保存原始 Search:', originalSearch) console.log('💾 已保存原始 Hash:', originalHash) } catch (e) { console.warn('⚠️ 保存原始 URL 失败:', e) } // 导出标志,供 axios 拦截器使用 export let isAuthenticating = false /** * 从 URL 获取票据参数 */ export function getTicketFromUrl() { console.log('🔍 === 开始获取票据参数 ===') console.log('📍 当前完整 URL:', window.location.href) console.log('📍 当前 location.search:', window.location.search) console.log('📍 当前 location.hash:', window.location.hash) console.log('📍 保存的原始 URL:', originalUrl) console.log('📍 保存的原始 Search:', originalSearch) console.log('📍 保存的原始 Hash:', originalHash) // 新的 URL 格式: http://域名/?iamcaspticket=xxx // 优先从保存的原始 URL 中获取(防止页面跳转导致票据丢失) // 如果原始 URL 中没有,再从当前 URL 获取 let search = '' // ===== 优先级1: 从保存的原始 URL 中获取 ===== if (originalSearch) { search = originalSearch console.log('✅ 从保存的原始 Search 中提取到查询参数:', search.substring(0, 100)) } else if (originalHash && originalHash.includes('?')) { const hashParts = originalHash.split('?') if (hashParts.length > 1) { search = '?' + hashParts.slice(1).join('?') console.log('✅ 从保存的原始 Hash 中提取到查询参数:', search.substring(0, 100)) } } // ===== 优先级2: 如果原始 URL 中没有,从当前 URL 获取 ===== if (!search) { if (window.location.search) { search = window.location.search console.log('✅ 从当前 Search 中提取到查询参数:', search.substring(0, 100)) } if (!search && window.location.hash && window.location.hash.includes('?')) { const hashParts = window.location.hash.split('?') if (hashParts.length > 1) { search = '?' + hashParts.slice(1).join('?') console.log('✅ 从当前 Hash 中提取到查询参数(兼容模式):', search.substring(0, 100)) } } } if (!search) { console.log('❌ 未找到查询参数') console.log('🔍 === 票据获取结束 ===') return null } console.log('📍 原始查询字符串:', search) console.log('📍 查询字符串长度:', search.length) console.log('📍 查询字符串前10个字符:', search.substring(0, 10)) // 处理 ?& 格式异常(多余的 & 符号) if (search.startsWith('?&')) { console.log('⚠️ 检测到 URL 格式异常: ?& 开头,正在修正...') search = '?' + search.substring(2) // 去掉 ?& 中的 &,保留 ? console.log('✅ 修正后的查询字符串:', search.substring(0, 100)) } // 尝试多种方式解析票据 let ticket = null // 方式1: 使用 URLSearchParams const urlParams = new URLSearchParams(search) ticket = urlParams.get('iamcaspticket') console.log('📋 方式1 (URLSearchParams) 结果:', ticket ? ticket.substring(0, 50) + '...' : 'null') // 方式2: 如果方式1失败,尝试手动解析(处理特殊情况) if (!ticket && search) { console.log('⚠️ URLSearchParams 解析失败,尝试手动解析...') // 移除开头的 ? 或 ?& let cleanSearch = search if (cleanSearch.startsWith('?&')) { cleanSearch = cleanSearch.substring(2) } else if (cleanSearch.startsWith('?')) { cleanSearch = cleanSearch.substring(1) } console.log('📋 清理后的字符串:', cleanSearch.substring(0, 50) + '...') // 按 & 分割参数 const params = cleanSearch.split('&') console.log('📋 分割后的参数数量:', params.length) for (const param of params) { // 只分割第一个 = 号,避免票据内容中的 = 被分割 const equalIndex = param.indexOf('=') if (equalIndex === -1) continue const key = param.substring(0, equalIndex) const value = param.substring(equalIndex + 1) console.log('📋 检查参数:', key) if (key === 'iamcaspticket' && value) { ticket = decodeURIComponent(value) console.log('✅ 方式2 (手动解析) 找到票据:', ticket.substring(0, 50) + '...') break } } } if (ticket) { console.log('🎫 成功获取到票据!') console.log('🎫 票据长度:', ticket.length) console.log('🎫 票据前50个字符:', ticket.substring(0, 50) + '...') // 票据可能已经解码,检查是否需要再次解码 try { // 如果票据中包含 %,说明可能是 URL 编码的 if (ticket.includes('%')) { const decoded = decodeURIComponent(ticket) console.log('🔄 票据已解码') ticket = decoded } } catch (e) { console.log('⚠️ 票据解码失败,使用原始值:', e.message) } console.log('🔍 === 票据获取成功 ===') return ticket } console.log('❌ 未找到票据参数 iamcaspticket') console.log('📍 当前 URL:', window.location.href) console.log('📍 查询字符串:', search) console.log('🔍 === 票据获取结束 ===') return null } /** * 清理 URL 中的票据参数 * 票据只能使用一次,处理成功后应该清理,避免刷新时重复使用 */ export function clearTicketFromUrl() { try { console.log('='.repeat(60)) console.log('🧹 开始清理 URL 中的票据参数...') console.log('📍 调用栈:', new Error().stack) console.log('📍 当前 URL:', window.location.href) // 获取当前 URL const currentUrl = window.location.href const url = new URL(currentUrl) const ticketParamsToRemove = ['iamcaspticket', 'iamtarget', 'ip'] let hasRemovedParams = false // 优先从普通查询参数中清理(新的标准格式) for (const param of ticketParamsToRemove) { if (url.searchParams.has(param)) { console.log(`✅ 从查询参数中删除: ${param}`) url.searchParams.delete(param) hasRemovedParams = true } } if (hasRemovedParams) { window.history.replaceState(null, '', url.toString()) console.log('✅ URL 已更新(普通模式)') console.log('📍 新 URL:', window.location.href) return } // 如果普通查询参数中没有,尝试从 hash 中清理(兼容旧格式 Hash 路由) if (url.hash && url.hash.includes('?')) { const hashParts = url.hash.split('?') const hashPath = hashParts[0] let hashSearch = hashParts.slice(1).join('?') console.log('📍 Hash 路径:', hashPath) console.log('📍 Hash 原始查询:', hashSearch) if (hashSearch.startsWith('&')) { console.log('⚠️ 检测到 Hash 查询参数以 & 开头,正在修正...') hashSearch = hashSearch.substring(1) console.log('✅ 修正后的 Hash 查询:', hashSearch) } const hashParams = new URLSearchParams(hashSearch) console.log('📋 Hash 查询参数列表:') for (const [key, value] of hashParams.entries()) { console.log(` - ${key}: ${value.substring(0, 50)}${value.length > 50 ? '...' : ''}`) } let hasRemovedHashParams = false for (const param of ticketParamsToRemove) { if (hashParams.has(param)) { console.log(`✅ 从 Hash 中删除参数(兼容模式): ${param}`) hashParams.delete(param) hasRemovedHashParams = true } } if (hasRemovedHashParams) { const newHashSearch = hashParams.toString() const newHash = newHashSearch ? `${hashPath}?${newHashSearch}` : hashPath console.log('📍 新的 Hash:', newHash) console.log('📍 当前 Hash:', window.location.hash) try { console.log('🔄 开始更新地址栏...') window.location.hash = newHash console.log('✅ 地址栏已更新(兼容模式)') console.log('📍 新的 Hash:', window.location.hash) console.log('📍 完整 URL:', window.location.href) } catch (e) { console.error('❌ 更新地址栏失败:', e) const newUrl = url.origin + url.pathname + newHash window.history.replaceState(null, '', newUrl) console.log('✅ 使用 replaceState 更新(降级方案)') } return } } console.log('ℹ️ URL 中没有票据参数,无需清理') } catch (error) { console.error('❌ 清理票据参数失败:', error) console.error('❌ 错误详情:', error.message) console.error('❌ 错误堆栈:', error.stack) // 清理失败不影响主流程 } } /** * 处理票据,获取访问令牌 */ export async function processTicket(ticketData) { try { console.log('🔍 正在处理票据...') console.log('📡 请求接口:', TICKET_PROCESS_API) console.log('📦 票据数据长度:', ticketData.length) console.log('📦 票据前100字符:', ticketData.substring(0, 100)) const requestBody = { ticket_data: ticketData } console.log('📤 发送请求...') console.log('📤 请求体:', JSON.stringify(requestBody).substring(0, 150)) const response = await fetch(TICKET_PROCESS_API, { method: 'POST', headers: { 'Content-Type': 'application/json', // 票据处理接口不需要 token,这是获取 token 的第一步 // 如果后端要求,可以添加一个特殊标识 'X-Auth-Type': 'ticket' }, body: JSON.stringify(requestBody), // 添加 credentials 以确保跨域请求携带cookie credentials: 'include' }) console.log('📥 收到响应,状态码:', response.status) if (!response.ok) { const errorText = await response.text() console.error('❌ HTTP错误响应:', errorText) throw new Error(`HTTP错误: ${response.status} - ${errorText}`) } const data = await response.json() console.log('📋 响应数据:', data) // 检查是否有 refresh_token if (data.refresh_token && data.token_type) { console.log('✅ 票据处理成功') console.log('🎫 Token类型:', data.token_type) console.log('🔑 Refresh Token:', data.refresh_token.substring(0, 50) + '...') // 如果返回了 username,也一起返回 if (data.username) { console.log('👤 用户名:', data.username) } return { refreshToken: data.refresh_token, tokenType: data.token_type, username: data.username || null } } else { console.error('❌ 响应数据格式错误,缺少必要字段') throw new Error('票据处理失败: 响应数据不完整') } } catch (error) { console.error('❌ 票据处理失败:', error) console.error('❌ 错误详情:', error.message) throw error } } /** * 保存令牌到 localStorage */ export function saveToken(refreshToken, tokenType, username = null) { try { console.log('💾 开始保存令牌...') console.log('🎫 Token类型:', tokenType) console.log('🔑 Refresh Token:', refreshToken.substring(0, 50) + '...') // 保存 refresh_token 和 token_type localStorage.setItem('shudao_refresh_token', refreshToken) localStorage.setItem('shudao_token_type', tokenType) // 如果有 username,也保存 if (username) { localStorage.setItem('shudao_username', username) console.log('👤 用户名:', username) } console.log('=' .repeat(60)) console.log('✅ 令牌保存完成!') console.log(' - shudao_refresh_token:', localStorage.getItem('shudao_refresh_token')?.substring(0, 50) + '...') console.log(' - shudao_token_type:', localStorage.getItem('shudao_token_type')) if (username) { console.log(' - shudao_username:', localStorage.getItem('shudao_username')) } console.log('=' .repeat(60)) return true } catch (error) { console.error('❌ 保存令牌失败:', error) throw error } } // ===== 已废弃:模拟用户生成功能 ===== // 现在完全依赖票据认证,不再使用降级方案 /** * 生成模拟用户信息(降级方案 - 已废弃) * 当票据解析失败或没有票据时使用 */ // export function generateMockUserInfo() { // console.log('=' .repeat(60)) // console.log('🎭 【降级方案】开始生成模拟用户信息...') // console.log('⚠️ 这是模拟数据,不是真实用户!') // console.log('=' .repeat(60)) // // // 生成随机用户ID (1-1000) // const mockUserId = Math.floor(Math.random() * 1000) + 1 // // // 生成模拟账号ID // const mockAccountId = `mock_user_${mockUserId}` // // // 生成模拟用户信息 // const mockUserInfo = { // accountID: mockAccountId, // name: `测试用户${mockUserId}`, // empNo: `TEST${String(mockUserId).padStart(6, '0')}`, // phone: `1380000${String(mockUserId).padStart(4, '0')}`, // mobile: `1380000${String(mockUserId).padStart(4, '0')}`, // username: `测试用户${mockUserId}` // } // // // 保存到 localStorage // localStorage.setItem('shudao_user_id', mockUserId.toString()) // localStorage.setItem('shudao_user_info', JSON.stringify(mockUserInfo)) // // // 生成模拟 token // const mockToken = `mock_token_${Date.now()}_${mockUserId}` // localStorage.setItem('shudao_access_token', mockToken) // // console.log('✅ 模拟用户信息已生成并保存:') // console.log(' - User ID:', mockUserId) // console.log(' - Account ID:', mockAccountId) // console.log(' - Name:', mockUserInfo.name) // console.log(' - User Info:', mockUserInfo) // console.log('=' .repeat(60)) // console.log('⚠️ 当前使用【模拟用户】,请检查是否有有效票据!') // console.log('=' .repeat(60)) // // return mockUserInfo // } /** * 检查本地是否已有令牌 * @returns {boolean} 是否有有效的本地令牌 */ function hasLocalToken() { try { const refreshToken = localStorage.getItem('shudao_refresh_token') const tokenType = localStorage.getItem('shudao_token_type') const hasData = !!(refreshToken && tokenType) if (hasData) { console.log('✅ 检测到本地已有令牌:') console.log(' - Refresh Token:', refreshToken.substring(0, 50) + '...') console.log(' - Token Type:', tokenType) } else { console.log('⚠️ 本地没有完整的令牌') console.log(' - Refresh Token:', refreshToken ? '有' : '无') console.log(' - Token Type:', tokenType ? '有' : '无') } return hasData } catch (error) { console.error('❌ 检查本地令牌失败:', error) return false } } /** * 获取本地令牌 * @returns {object|null} 本地令牌对象,如果不存在返回null */ function getLocalToken() { try { const refreshToken = localStorage.getItem('shudao_refresh_token') const tokenType = localStorage.getItem('shudao_token_type') if (!refreshToken || !tokenType) { return null } return { refreshToken, tokenType } } catch (error) { console.error('❌ 获取本地令牌失败:', error) return null } } // 调试日志收集器 const debugLogs = [] function addDebugLog(level, message) { const log = { level, time: new Date().toLocaleTimeString(), message } debugLogs.push(log) console.log(`[${level.toUpperCase()}] ${message}`) // 保存到 sessionStorage try { sessionStorage.setItem('auth_debug_logs', JSON.stringify(debugLogs)) } catch (e) { console.warn('无法保存调试日志:', e) } } /** * 处理票据认证流程 * 1. 从 URL 获取票据 * 2. 调用票据处理接口 * 3. 保存令牌 * 4. 认证策略: * - 有票据 + 处理成功 → 正常进入 * - 有票据 + 处理失败 → 跳转404 * - 无票据 + 有本地令牌 → 使用本地令牌进入 * - 无票据 + 无本地令牌 → 跳转404 * * @returns {object} 认证结果对象 { success: boolean, token: object, fromTicket/fromCache: boolean } * @throws {Error} 票据验证失败或无有效认证信息时抛出错误 */ export async function handleTicketAuth() { // 设置认证标志,防止 axios 拦截器在认证期间跳转 isAuthenticating = true // 清空之前的日志 debugLogs.length = 0 addDebugLog('info', '🚀 开始票据认证流程') addDebugLog('info', `当前 URL: ${window.location.href}`) addDebugLog('info', `原始 URL: ${originalUrl}`) addDebugLog('info', `User Agent: ${navigator.userAgent.substring(0, 100)}...`) addDebugLog('info', `是否移动端: ${/Mobile|Android|iPhone|iPad/i.test(navigator.userAgent)}`) try { // 1. 获取票据 addDebugLog('info', '步骤1: 获取票据') const ticket = getTicketFromUrl() if (!ticket) { addDebugLog('warning', '⚠️ 未找到票据') // 检查本地是否已有令牌 if (hasLocalToken()) { isAuthenticating = false addDebugLog('success', '✅ 本地已有令牌,使用本地令牌') const tokenData = getLocalToken() return { success: true, token: tokenData, fromCache: true } } else { // 未找到票据且本地无令牌,抛出错误 addDebugLog('error', '❌ 未找到票据且本地无令牌') throw new Error('TICKET_NOT_FOUND') } } addDebugLog('success', `✅ 成功获取票据 (长度: ${ticket.length})`) // 2. 处理票据 addDebugLog('info', '步骤2: 调用后端处理票据') const { refreshToken, tokenType, username } = await processTicket(ticket) addDebugLog('success', `✅ 票据处理成功,获得 token`) // 3. 保存令牌和用户名 addDebugLog('info', '步骤3: 保存令牌到本地') saveToken(refreshToken, tokenType, username) addDebugLog('success', `✅ 令牌已保存 (用户: ${username || '未知'})`) addDebugLog('success', '🎉 票据认证流程完成!') // 认证完成,清除标志 isAuthenticating = false return { success: true, token: { refreshToken, tokenType, username }, fromTicket: true } } catch (error) { addDebugLog('error', `❌ 认证失败: ${error.message}`) addDebugLog('error', `错误堆栈: ${error.stack?.substring(0, 200)}...`) // 认证失败,清除标志 isAuthenticating = false // 票据验证失败,直接抛出错误 throw error } } /** * 清理 URL 中的票据参数 */ function cleanUrlParams() { try { const url = new URL(window.location.href) url.searchParams.delete('iamcaspticket') // 使用 replaceState 更新 URL,不刷新页面 window.history.replaceState({}, document.title, url.toString()) console.log('🧹 已清理 URL 中的票据参数') } catch (error) { console.error('清理 URL 参数失败:', error) } }