diff --git a/public/index.html b/public/index.html index 89c0ca5..12ca9ef 100644 --- a/public/index.html +++ b/public/index.html @@ -91,6 +91,121 @@ [data-theme="dark"] p { color: #ffffff; } + + /* 阿里云盘扫码v2样式 */ + .qr-modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + } + + .qr-modal-content { + background-color: #fefefe; + margin: 5% auto; + padding: 20px; + border-radius: 10px; + width: 90%; + max-width: 500px; + text-align: center; + } + + [data-theme="dark"] .qr-modal-content { + background-color: #2a2a3b; + color: #f0f0f0; + } + + .qr-code-container { + margin: 20px 0; + padding: 20px; + background: #f8f9fa; + border-radius: 10px; + } + + [data-theme="dark"] .qr-code-container { + background: #1a1a2e; + } + + .qr-code-img { + max-width: 200px; + max-height: 200px; + border-radius: 8px; + } + + .qr-status { + margin: 15px 0; + padding: 10px; + border-radius: 5px; + font-weight: 500; + } + + .qr-status.waiting { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; + } + + .qr-status.scaned { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; + } + + .qr-status.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + .qr-status.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + + [data-theme="dark"] .qr-status.waiting { + background: #3d3d00; + color: #ffeb3b; + border: 1px solid #ffeb3b; + } + + [data-theme="dark"] .qr-status.scaned { + background: #003d4d; + color: #00bcd4; + border: 1px solid #00bcd4; + } + + [data-theme="dark"] .qr-status.success { + background: #1b5e20; + color: #4caf50; + border: 1px solid #4caf50; + } + + [data-theme="dark"] .qr-status.error { + background: #5d1a1a; + color: #f44336; + border: 1px solid #f44336; + } + + .close-btn { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; + } + + .close-btn:hover { + color: black; + } + + [data-theme="dark"] .close-btn:hover { + color: white; + } @@ -108,6 +223,7 @@ + @@ -165,6 +281,22 @@

+ + +
+
+ × +

阿里云盘扫码登录v2

+ + +
+ + +
+
+
diff --git a/src/aliui2.ts b/src/aliui2.ts new file mode 100644 index 0000000..87c1c43 --- /dev/null +++ b/src/aliui2.ts @@ -0,0 +1,692 @@ +import {Context} from "hono"; +import * as local from "hono/cookie"; + +// 阿里云盘扫码登录相关接口定义 +interface QRCodeData { + qrCodeUrl: string; + ck?: string; + t?: string; + resultCode?: number; + processFinished?: boolean; +} + +interface QRStatusResponse { + success: boolean; + content: { + qrCodeStatus: string; + resultCode: number; + bizExt?: any; + data?: any; + }; +} + +interface UserInfo { + user_id: string; + nick_name: string; + avatar: string; + phone: string; + email: string; +} + +interface DriveInfo { + total_size: number; + used_size: number; + album_drive_used_size: number; + note_drive_used_size: number; +} + +// 阿里云盘API响应接口 +interface AliCloudApiResponse { + hasError?: boolean; + content?: { + success?: boolean; + data?: { + codeContent?: string; + ck?: string; + t?: string; + resultCode?: number; + processFinished?: boolean; + qrCodeStatus?: string; + bizExt?: any; + }; + }; +} + +// 阿里云盘扫码登录类 +class AlipanQRLogin { + private session_id: string; + private csrf_token: string; + private umid_token: string; + private qr_code_data: QRCodeData | null = null; + private access_token: string | null = null; + private refresh_token: string | null = null; + + constructor() { + this.session_id = this.generateUUID(); + this.csrf_token = "MuSysYVxW5AMGblcOTSKb3"; + this.umid_token = this.generateUUID().replace(/-/g, ''); + } + + private generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + private getHeaders(): Record { + return { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + 'Referer': 'https://passport.alipan.com/', + 'Origin': 'https://passport.alipan.com' + }; + } + + // 获取OAuth认证URL + async getOAuthUrl(): Promise { + try { + const authUrl = "https://auth.alipan.com/v2/oauth/authorize"; + const params = new URLSearchParams({ + 'client_id': '25dzX3vbYqktVxyX', + 'redirect_uri': 'https://www.alipan.com/sign/callback', + 'response_type': 'code', + 'login_type': 'custom', + 'state': '{"origin":"https://www.alipan.com"}' + }); + + const response = await fetch(`${authUrl}?${params}`, { + method: 'GET', + headers: this.getHeaders() + }); + + if (response.ok) { + return response.url; + } + return null; + } catch (error) { + console.error('获取OAuth URL失败:', error); + return null; + } + } + + // 获取登录页面信息 + async getLoginPage(): Promise { + try { + const loginUrl = "https://passport.alipan.com/mini_login.htm"; + const params = new URLSearchParams({ + 'lang': 'zh_cn', + 'appName': 'aliyun_drive', + 'appEntrance': 'web_default', + 'styleType': 'auto', + 'bizParams': '', + 'notLoadSsoView': 'false', + 'notKeepLogin': 'false', + 'isMobile': 'false', + 'ad__pass__q__rememberLogin': 'true', + 'ad__pass__q__rememberLoginDefaultValue': 'true', + 'ad__pass__q__forgotPassword': 'true', + 'ad__pass__q__licenseMargin': 'true', + 'ad__pass__q__loginType': 'normal', + 'hidePhoneCode': 'true', + 'rnd': Date.now().toString() + }); + + const response = await fetch(`${loginUrl}?${params}`, { + method: 'GET', + headers: this.getHeaders() + }); + + if (response.ok) { + const content = await response.text(); + + // 尝试提取CSRF token + const csrfMatch = content.match(/_csrf_token["']?\s*[:=]\s*["']([^"']+)["']/); + if (csrfMatch) { + this.csrf_token = csrfMatch[1]; + } + + // 尝试提取umidToken + const umidMatch = content.match(/umidToken["']?\s*[:=]\s*["']([^"']+)["']/); + if (umidMatch) { + this.umid_token = umidMatch[1]; + } + + return true; + } + return false; + } catch (error) { + console.error('获取登录页面失败:', error); + return false; + } + } + + // 生成二维码 + async generateQRCode(): Promise { + try { + const qrUrl = "https://passport.alipan.com/newlogin/qrcode/generate.do"; + const params = new URLSearchParams({ + 'appName': 'aliyun_drive', + 'fromSite': '52', + 'appEntrance': 'web_default', + '_csrf_token': this.csrf_token, + 'umidToken': this.umid_token, + 'hsiz': '115d9f5f2cf2f87850a93a793aaaecb4', + 'bizParams': 'taobaoBizLoginFrom=web_default&renderRefer=https%3A%2F%2Fauth.alipan.com%2F', + 'mainPage': 'false', + 'isMobile': 'false', + 'lang': 'zh_CN', + 'returnUrl': '', + 'umidTag': 'SERVER' + }); + + const headers = { + ...this.getHeaders(), + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }; + + const response = await fetch(`${qrUrl}?${params}`, { + method: 'GET', + headers: headers + }); + + if (response.ok) { + const result = await response.json() as AliCloudApiResponse; + + if (!result.hasError) { + const content = result.content || {}; + if (content.success) { + const data = content.data || {}; + const codeContent = data.codeContent; + + if (codeContent) { + this.qr_code_data = { + qrCodeUrl: codeContent, + ck: data.ck, + t: data.t, + resultCode: data.resultCode, + processFinished: data.processFinished + }; + return this.qr_code_data; + } + } + } + } + return null; + } catch (error) { + console.error('生成二维码失败:', error); + return null; + } + } + + // 查询二维码状态 + async queryQRStatus(): Promise { + try { + if (!this.qr_code_data) { + return null; + } + + const queryUrl = "https://passport.alipan.com/newlogin/qrcode/query.do"; + const formData = new URLSearchParams({ + 'appName': 'aliyun_drive', + 'fromSite': '52' + }); + + if (this.qr_code_data.ck) { + formData.append('ck', this.qr_code_data.ck); + } + if (this.qr_code_data.t) { + formData.append('t', this.qr_code_data.t); + } + + const headers = { + ...this.getHeaders(), + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }; + + const response = await fetch(queryUrl, { + method: 'POST', + headers: headers, + body: formData + }); + + if (response.ok) { + const result = await response.json() as AliCloudApiResponse; + + if (!result.hasError) { + const content = result.content || {}; + if (content.success) { + const data = content.data || {}; + const apiQrStatus = data.qrCodeStatus || 'NEW'; + const resultCode = data.resultCode || 0; + + // 状态映射 + const statusMapping: Record = { + 'NEW': 'WAITING', + 'SCANED': 'SCANED', + 'CONFIRMED': 'CONFIRMED', + 'EXPIRED': 'EXPIRED' + }; + + const qrCodeStatus = statusMapping[apiQrStatus] || 'WAITING'; + + return { + success: true, + content: { + qrCodeStatus: qrCodeStatus, + resultCode: resultCode, + bizExt: data.bizExt || {}, + data: data + } + }; + } + } + } + return null; + } catch (error) { + console.error('查询二维码状态失败:', error); + return null; + } + } + + // 获取访问令牌 + async getAccessToken(bizExt: any): Promise { + try { + console.log('getAccessToken - bizExt type:', typeof bizExt); + console.log('getAccessToken - bizExt:', bizExt); + + // bizExt 是 Base64 编码的字符串,需要先解码 + let decodedBizExt: any; + if (typeof bizExt === 'string') { + try { + const decodedString = atob(bizExt); + decodedBizExt = JSON.parse(decodedString); + console.log('getAccessToken - decoded bizExt:', JSON.stringify(decodedBizExt, null, 2)); + } catch (decodeError) { + console.error('解码 bizExt 失败:', decodeError); + return null; + } + } else { + decodedBizExt = bizExt; + } + + if (!decodedBizExt || !decodedBizExt.pds_login_result) { + console.log('getAccessToken - No pds_login_result found in decoded data'); + return null; + } + + const loginResult = decodedBizExt.pds_login_result; + console.log('getAccessToken - loginResult:', JSON.stringify(loginResult, null, 2)); + this.access_token = loginResult.accessToken; + this.refresh_token = loginResult.refreshToken; + console.log('getAccessToken - access_token set:', this.access_token ? 'success' : 'failed'); + console.log('getAccessToken - refresh_token set:', this.refresh_token ? 'success' : 'failed'); + return this.access_token; + } catch (error) { + console.error('获取访问令牌失败:', error); + return null; + } + } + + // 获取用户信息 + async getUserInfo(): Promise { + try { + if (!this.access_token) { + return null; + } + + const userUrl = "https://user.aliyundrive.com/v2/user/get"; + const headers = { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json' + }; + + const response = await fetch(userUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify({}) + }); + + if (response.ok) { + return await response.json() as UserInfo; + } + return null; + } catch (error) { + console.error('获取用户信息失败:', error); + return null; + } + } + + // 获取网盘信息 + async getDriveInfo(): Promise { + try { + if (!this.access_token) { + return null; + } + + const driveUrl = "https://api.aliyundrive.com/adrive/v1/user/driveCapacityDetails"; + const headers = { + 'Authorization': `Bearer ${this.access_token}`, + 'Content-Type': 'application/json' + }; + + const response = await fetch(driveUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify({}) + }); + + if (response.ok) { + return await response.json() as DriveInfo; + } + return null; + } catch (error) { + console.error('获取网盘信息失败:', error); + return null; + } + } + + // 检查是否已登录 + isLoggedIn(): boolean { + return !!this.access_token; + } + + // 获取访问令牌(用于返回给前端) + getToken(): string | null { + return this.access_token; + } + + // 获取刷新令牌 + getRefreshToken(): string | null { + return this.refresh_token; + } +} + +// 会话管理接口 +interface SessionData { + instance: AlipanQRLogin; + createdAt: number; + lastAccess: number; + clientFingerprint?: string; +} + +// 全局实例存储 - 改为存储会话数据而不是直接存储实例 +const loginSessions = new Map(); + +// 会话过期时间(30分钟) +const SESSION_TIMEOUT = 30 * 60 * 1000; + +// 生成安全的会话ID +function generateSecureSessionId(): string { + const timestamp = Date.now().toString(36); + const randomPart = Math.random().toString(36).substring(2, 15); + const randomPart2 = Math.random().toString(36).substring(2, 15); + return `${timestamp}-${randomPart}-${randomPart2}`; +} + +// 清理过期会话 +function cleanupExpiredSessions() { + const now = Date.now(); + for (const [sessionId, sessionData] of loginSessions.entries()) { + if (now - sessionData.lastAccess > SESSION_TIMEOUT) { + loginSessions.delete(sessionId); + console.log(`清理过期会话: ${sessionId}`); + } + } +} + +// 注意:不能在全局作用域使用 setInterval,改为在每次请求时检查过期会话 + +// 获取或创建会话 +function getOrCreateSession(sessionId?: string, clientFingerprint?: string): { sessionId: string, sessionData: SessionData } { + const now = Date.now(); + + // 如果提供了sessionId,尝试获取现有会话 + if (sessionId && loginSessions.has(sessionId)) { + const sessionData = loginSessions.get(sessionId)!; + + // 检查会话是否过期 + if (now - sessionData.lastAccess > SESSION_TIMEOUT) { + loginSessions.delete(sessionId); + console.log(`会话已过期,删除: ${sessionId}`); + } else { + // 更新最后访问时间 + sessionData.lastAccess = now; + return { sessionId, sessionData }; + } + } + + // 创建新会话 + const newSessionId = generateSecureSessionId(); + const newSessionData: SessionData = { + instance: new AlipanQRLogin(), + createdAt: now, + lastAccess: now, + clientFingerprint + }; + + loginSessions.set(newSessionId, newSessionData); + console.log(`创建新会话: ${newSessionId}, 客户端指纹: ${clientFingerprint || 'none'}`); + + return { sessionId: newSessionId, sessionData: newSessionData }; +} + +// 验证会话所有权(可选的额外安全措施) +function validateSessionOwnership(sessionId: string, clientFingerprint?: string): boolean { + const sessionData = loginSessions.get(sessionId); + if (!sessionData) return false; + + // 如果设置了客户端指纹,进行验证 + if (sessionData.clientFingerprint && clientFingerprint) { + return sessionData.clientFingerprint === clientFingerprint; + } + + return true; +} + +// 生成二维码接口 +export async function generateQR(c: Context) { + try { + // 清理过期会话 + cleanupExpiredSessions(); + + const requestedSessionId = c.req.query('session_id'); + const clientFingerprint = c.req.header('X-Client-Fingerprint') || c.req.header('User-Agent'); + + // 获取或创建会话 + const { sessionId, sessionData } = getOrCreateSession(requestedSessionId, clientFingerprint); + const alipan = sessionData.instance; + + // 获取OAuth URL + const oauthUrl = await alipan.getOAuthUrl(); + if (!oauthUrl) { + return c.json({error: '获取OAuth URL失败,请检查网络连接'}, 500); + } + + // 获取登录页面 + const loginPageResult = await alipan.getLoginPage(); + if (!loginPageResult) { + return c.json({error: '获取登录页面失败,请检查网络连接'}, 500); + } + + // 生成二维码 + const qrData = await alipan.generateQRCode(); + if (!qrData) { + return c.json({error: '生成二维码失败,可能是网络问题或API变化,请稍后重试'}, 500); + } + + console.log(`会话 ${sessionId} 生成二维码成功`); + + return c.json({ + success: true, + session_id: sessionId, + qr_code_url: qrData.qrCodeUrl, + message: '二维码生成成功,请使用阿里云盘App扫码登录', + expires_in: SESSION_TIMEOUT / 1000 // 返回过期时间(秒) + }); + + } catch (error) { + console.error('生成二维码失败:', error); + return c.json({error: '生成二维码失败'}, 500); + } +} + +// 检查登录状态接口 +export async function checkLogin(c: Context) { + try { + const sessionId = c.req.query('session_id'); + if (!sessionId) { + return c.json({error: '缺少session_id参数'}, 400); + } + + const clientFingerprint = c.req.header('X-Client-Fingerprint') || c.req.header('User-Agent'); + + // 验证会话所有权 + if (!validateSessionOwnership(sessionId, clientFingerprint)) { + return c.json({error: '会话验证失败'}, 403); + } + + const sessionData = loginSessions.get(sessionId); + if (!sessionData) { + return c.json({error: '会话不存在或已过期'}, 404); + } + + // 更新最后访问时间 + sessionData.lastAccess = Date.now(); + + const alipan = sessionData.instance; + + // 查询二维码状态 + const statusResult = await alipan.queryQRStatus(); + if (!statusResult) { + return c.json({error: '查询登录状态失败'}, 500); + } + + const status = statusResult.content.qrCodeStatus; + + // 如果登录成功,获取访问令牌 + if (status === 'CONFIRMED') { + const accessToken = await alipan.getAccessToken(statusResult.content.bizExt); + console.log(`会话 ${sessionId} - 登录确认,token获取: ${accessToken ? '成功' : '失败'}`); + if (accessToken) { + return c.json({ + success: true, + status: 'CONFIRMED', + message: '登录成功', + access_token: accessToken + }); + } + } + + // 状态消息映射 + const statusMessages: Record = { + 'WAITING': '等待扫描', + 'SCANED': '已扫描,等待确认', + 'CONFIRMED': '登录成功', + 'EXPIRED': '二维码已过期' + }; + + return c.json({ + success: true, + status: status, + message: statusMessages[status] || '未知状态' + }); + + } catch (error) { + console.error('检查登录状态失败:', error); + return c.json({error: '检查登录状态失败'}, 500); + } +} + +// 获取用户信息接口 +export async function getUserInfo(c: Context) { + try { + const sessionId = c.req.query('session_id'); + if (!sessionId) { + return c.json({error: '缺少session_id参数'}, 400); + } + + const clientFingerprint = c.req.header('X-Client-Fingerprint') || c.req.header('User-Agent'); + + // 验证会话所有权 + if (!validateSessionOwnership(sessionId, clientFingerprint)) { + return c.json({error: '会话验证失败'}, 403); + } + + const sessionData = loginSessions.get(sessionId); + if (!sessionData) { + return c.json({error: '会话不存在或已过期'}, 404); + } + + // 更新最后访问时间 + sessionData.lastAccess = Date.now(); + + const alipan = sessionData.instance; + + // 检查是否已经登录成功 + console.log(`会话 ${sessionId} - 登录状态: ${alipan.isLoggedIn()}, token: ${alipan.getToken() ? '存在' : '不存在'}`); + if (!alipan.isLoggedIn()) { + return c.json({error: '用户尚未登录成功,请先完成扫码登录'}, 400); + } + + // 获取用户信息 + const userInfo = await alipan.getUserInfo(); + if (!userInfo) { + return c.json({error: '获取用户信息失败,可能是token已过期'}, 500); + } + + // 获取网盘信息 + const driveInfo = await alipan.getDriveInfo(); + + return c.json({ + success: true, + user_info: userInfo, + drive_info: driveInfo, + access_token: alipan.getToken(), + refresh_token: alipan.getRefreshToken() + }); + + } catch (error) { + console.error('获取用户信息失败:', error); + return c.json({error: '获取用户信息失败'}, 500); + } +} + +// 退出登录接口 +export async function logout(c: Context) { + try { + const sessionId = c.req.query('session_id'); + if (!sessionId) { + return c.json({error: '缺少session_id参数'}, 400); + } + + const clientFingerprint = c.req.header('X-Client-Fingerprint') || c.req.header('User-Agent'); + + // 验证会话所有权 + if (!validateSessionOwnership(sessionId, clientFingerprint)) { + return c.json({error: '会话验证失败'}, 403); + } + + // 删除会话 + const deleted = loginSessions.delete(sessionId); + console.log(`会话 ${sessionId} 退出登录: ${deleted ? '成功' : '会话不存在'}`); + + return c.json({ + success: true, + message: '退出登录成功' + }); + + } catch (error) { + console.error('退出登录失败:', error); + return c.json({error: '退出登录失败'}, 500); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 82713c3..c806b34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import manifest from '__STATIC_CONTENT_MANIFEST' import * as local from "hono/cookie"; import * as oneui from './oneui'; import * as aliui from './aliui'; +import * as aliui2 from './aliui2'; import * as ui115 from './115ui'; import * as ui123 from './123ui'; import * as baidu from './baidu'; @@ -40,6 +41,26 @@ app.get('/alicloud/callback', async (c: Context) => { return aliui.alyToken(c); }); +// 阿里云盘扫码2 - 生成二维码 ############################################################################## +app.get('/alicloud2/generate_qr', async (c: Context) => { + return aliui2.generateQR(c); +}); + +// 阿里云盘扫码2 - 检查登录状态 ############################################################################## +app.get('/alicloud2/check_login', async (c: Context) => { + return aliui2.checkLogin(c); +}); + +// 阿里云盘扫码2 - 获取用户信息 ############################################################################## +app.get('/alicloud2/get_user_info', async (c: Context) => { + return aliui2.getUserInfo(c); +}); + +// 阿里云盘扫码2 - 退出登录 ############################################################################## +app.get('/alicloud2/logout', async (c: Context) => { + return aliui2.logout(c); +}); + // 登录申请 ############################################################################## app.get('/baiduyun/requests', async (c: Context) => { return baidu.oneLogin(c);