| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- /**
- * 票据认证工具
- * 处理从门户传递过来的票据,获取访问令牌
- */
- // 票据处理接口(直接使用完整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)
- }
- }
|