CF部署的OneDrive令牌获取工具
一、 部署方式
- 登录 Cloudflare:
- 前往 Cloudflare 仪表板 并登录。
- 创建 Worker:
- 在左侧菜单中,找到并点击 Workers & Pages。
- 点击 创建应用程序 (Create Application),然后选择 创建 Worker (Create Worker)。
- 为您的 Worker 设置一个唯一的子域名(例如 my-onedrive-tool),然后点击 部署 (Deploy)。
- 编辑并粘贴代码:
- 部署成功后,点击 编辑代码 (Edit code) 进入在线编辑器。
- 编辑器中会有一段默认的 "Hello World" 模板代码,请将其 全部删除。
- 将本仓库中的 worker.js 文件里的 全部代码 复制出来。
- 将代码粘贴到 Cloudflare 在线编辑器中。
- 保存并完成:
- 点击编辑器右上角的 保存并部署 (Save and Deploy) 按钮。
稍等片刻,部署就会完成。现在您可以通过访问您的 Worker URL (https://your-worker-name.your-subdomain.workers.dev) 来使用这个工具了!
// --- Backend API Logic --- /** * Handles CORS preflight requests. * @returns {Response} */ function handleOptions() { return new Response(null, { headers: getCorsHeaders(), }); } /** * Creates a JSON response with appropriate headers. * @param {object} body - The response body. * @param {number} status - The HTTP status code. * @returns {Response} */ function createJsonResponse(body, status) { return new Response(JSON.stringify(body, null, 2), { status: status, headers: { 'Content-Type': 'application/json', ...getCorsHeaders(), }, }); } /** * Gets the required CORS headers. * @returns {object} */ function getCorsHeaders() { return { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; } /** * Handles the token acquisition request from the frontend. * It exchanges the authorization code for an access token and refresh token. * @param {Request} request - The incoming request. * @returns {Promise<Response>} */ async function handleTokenRequest(request) { const { code, redirect_uri, cloud_env, client_id, client_secret } = await request.json(); if (!code || !redirect_uri || !cloud_env || !client_id || !client_secret) { return createJsonResponse({ error: '请求体中缺少必要参数。' }, 400); } const tokenUrl = cloud_env === 'china' ? 'https://login.chinacloudapi.cn/common/oauth2/v2.0/token' : 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; const body = new URLSearchParams({ client_id: client_id, client_secret: client_secret, code: code, redirect_uri: redirect_uri, grant_type: 'authorization_code', scope: 'offline_access Files.ReadWrite.All Sites.Read.All', }); try { const tokenResponse = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body, }); const tokenData = await tokenResponse.json(); if (!tokenResponse.ok) { return createJsonResponse(tokenData, tokenResponse.status); } return createJsonResponse(tokenData, 200); } catch (error) { return createJsonResponse({ error: '从微软获取令牌失败。', details: error.message }, 500); } } /** * Handles the SharePoint Site ID request from the frontend. * It uses the provided access token to query the Microsoft Graph API. * @param {Request} request - The incoming request. * @returns {Promise<Response>} */ async function handleSiteIdRequest(request) { const { accessToken, cloudEnv, hostname, sitePath } = await request.json(); if (!accessToken || !cloudEnv || !hostname) { return createJsonResponse({ error: '查询 Site ID 的请求体中缺少必要参数。' }, 400); } const graphEndpoint = cloudEnv === 'china' ? 'https://microsoftgraph.chinacloudapi.cn' : 'https://graph.microsoft.com'; // Construct the URL for the Graph API call const relativePath = sitePath && sitePath !== '/' ? `:${sitePath}` : ''; const siteUrl = `${graphEndpoint}/v1.0/sites/${hostname}${relativePath}`; try { const siteResponse = await fetch(siteUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, }, }); const siteData = await siteResponse.json(); if (!siteResponse.ok) { return createJsonResponse(siteData, siteResponse.status); } return createJsonResponse(siteData, 200); } catch (error) { return createJsonResponse({ error: '从微软 Graph 获取 Site ID 失败。', details: error.message }, 500); } } // --- Frontend HTML & JS --- /** * Generates the full HTML content for the tool's UI. * @returns {string} - The HTML content as a string. */ function getHtmlContent() { return ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>一体化 OneDrive & SharePoint 工具</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <style> body { font-family: 'Inter', 'Noto Sans SC', sans-serif; } .break-all { word-break: break-all; } .copy-btn { position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 0.375rem; background-color: #4B5563; /* bg-gray-600 */ color: white; cursor: pointer; border: none; opacity: 0.6; transition: opacity 0.2s; } .copy-btn:hover { opacity: 1; } .token-wrapper { position: relative; } .result-wrapper { position: relative; } </style> </head> <body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200"> <div class="container mx-auto p-4 md:p-8 max-w-4xl"> <header class="text-center mb-8"> <h1 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white">一体化 OneDrive & SharePoint 工具</h1> <p class="mt-2 text-gray-600 dark:text-gray-400">一键获取刷新令牌和 SharePoint Site ID</p> </header> <div class="bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-4 rounded-md mb-8" role="alert"> <p class="font-bold">安全警告</p> <p>在此页面填写客户端密码 (Client Secret) 存在一定风险。请仅在您自己的电脑和信任的网络环境下使用此工具。</p> </div> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8"> <h2 class="text-2xl font-semibold mb-4 border-b pb-2 dark:border-gray-600">步骤 1: 应用配置 & 获取令牌</h2> <div class="grid grid-cols-1 gap-6"> <div> <label for="clientId" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">应用 ID (Client ID)</label> <input type="text" id="clientId" placeholder="您的Azure应用ID" class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> <div> <label for="clientSecret" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">客户端密码 (Client Secret)</label> <input type="password" id="clientSecret" placeholder="您的Azure应用客户端密码" class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> <div> <label for="redirectUri" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">重定向 URI (Redirect URI)</label> <div class="relative"> <input type="text" id="redirectUri" readonly class="w-full pl-3 pr-16 py-2 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md shadow-sm"> <button id="copyRedirectUriBtn" class="copy-btn">复制</button> </div> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">将此URL复制到您Azure应用的Web平台重定向URI中。</p> </div> <div> <label for="cloudEnv" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">云环境</label> <select id="cloudEnv" class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="global">Microsoft 全球云 (Global)</option> <option value="china">由世纪互联运营的 Microsoft Azure (21Vianet)</option> </select> </div> </div> <div class="mt-6 text-center"> <button id="getAuthCodeBtn" class="px-8 py-3 bg-blue-600 text-white font-semibold rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900 transition-colors text-lg"> 开始认证 </button> </div> </div> <div id="result-container" class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 hidden"> <h2 class="text-2xl font-semibold mb-4 border-b pb-2 dark:border-gray-600">令牌结果</h2> <div id="spinner" class="hidden flex justify-center items-center my-4"> <svg class="animate-spin -ml-1 mr-3 h-8 w-8 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> <span class="text-lg">正在从后端获取令牌...</span> </div> <div id="result-content"></div> </div> <div id="site-id-finder" class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 hidden"> <h2 class="text-2xl font-semibold mb-4 border-b pb-2 dark:border-gray-600">步骤 2: 获取 SharePoint Site ID (可选)</h2> <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">使用上方获取的访问令牌来查询 Site ID。</p> <div class="grid grid-cols-1 gap-6"> <div> <label for="sharepointUrl" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SharePoint 网站 URL</label> <input type="url" id="sharepointUrl" placeholder="https://contoso.sharepoint.com/sites/MyTeamSite" class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> </div> <div class="text-center mt-6"> <button id="getSiteIdBtn" class="px-6 py-2 bg-green-600 text-white font-semibold rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 dark:focus:ring-offset-gray-900 transition-colors">获取 Site ID</button> </div> <div id="site-id-result-container" class="mt-6 hidden"> <div id="site-id-spinner" class="hidden flex justify-center items-center my-4"> <svg class="animate-spin -ml-1 mr-3 h-6 w-6 text-green-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> <span class="text-lg">正在查询 Site ID...</span> </div> <div id="site-id-result-content"></div> </div> </div> </div> <script> // --- DOM Elements --- const clientIdInput = document.getElementById('clientId'); const clientSecretInput = document.getElementById('clientSecret'); const cloudEnvSelect = document.getElementById('cloudEnv'); const redirectUriInput = document.getElementById('redirectUri'); const copyRedirectUriBtn = document.getElementById('copyRedirectUriBtn'); const getAuthCodeBtn = document.getElementById('getAuthCodeBtn'); const resultContainer = document.getElementById('result-container'); const resultContent = document.getElementById('result-content'); const spinner = document.getElementById('spinner'); const siteIdFinder = document.getElementById('site-id-finder'); const sharepointUrlInput = document.getElementById('sharepointUrl'); const getSiteIdBtn = document.getElementById('getSiteIdBtn'); const siteIdResultContainer = document.getElementById('site-id-result-container'); const siteIdSpinner = document.getElementById('site-id-spinner'); const siteIdResultContent = document.getElementById('site-id-result-content'); // --- App State --- let appState = { accessToken: null, cloudEnv: null }; const authEndpoints = { global: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', china: 'https://login.chinacloudapi.cn/common/oauth2/v2.0/authorize', }; // --- Utility Functions --- function copyToClipboard(text, button) { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); const originalText = button.textContent; button.textContent = '已复制!'; setTimeout(() => { button.textContent = originalText; }, 2000); } function showResult(title, content, isError = false) { spinner.classList.add('hidden'); resultContainer.classList.remove('hidden'); const titleColor = isError ? 'text-red-500 dark:text-red-400' : 'text-green-600 dark:text-green-400'; let formattedContent = \`\${content}\`; if (isError) { try { const errorJson = JSON.parse(content); formattedContent = \`<pre class="p-4 bg-gray-100 dark:bg-gray-900 rounded-md break-all font-mono text-sm">\${JSON.stringify(errorJson, null, 2)}</pre>\`; } catch(e) { formattedContent = \`<div class="p-4 bg-gray-100 dark:bg-gray-900 rounded-md break-all font-mono text-sm">\${content}</div>\`; } } resultContent.innerHTML = \`<h4 class="font-bold text-lg mb-2 \${titleColor}">\${title}</h4>\${formattedContent}\`; } function showSiteIdResult(title, content, isError = false) { siteIdSpinner.classList.add('hidden'); siteIdResultContainer.classList.remove('hidden'); const titleColor = isError ? 'text-red-500 dark:text-red-400' : 'text-green-600 dark:text-green-400'; let formattedContent = content; if (isError) { try { const errorJson = JSON.parse(content); formattedContent = \`<pre class="p-4 bg-gray-100 dark:bg-gray-900 rounded-md break-all font-mono text-sm">\${JSON.stringify(errorJson, null, 2)}</pre>\`; } catch(e) { formattedContent = \`<div class="p-4 bg-gray-100 dark:bg-gray-900 rounded-md break-all font-mono text-sm">\${content}</div>\`; } } siteIdResultContent.innerHTML = \`<h4 class="font-bold text-lg mb-2 \${titleColor}">\${title}</h4>\${formattedContent}\`; } // --- Core Logic --- function handleStartAuth() { const clientId = clientIdInput.value; const clientSecret = clientSecretInput.value; if (!clientId || !clientSecret) { alert('请输入应用 ID 和客户端密码'); return; } localStorage.setItem('ms_graph_tool_config', JSON.stringify({ clientId: clientId, clientSecret: clientSecret, cloudEnv: cloudEnvSelect.value, sharepointUrl: sharepointUrlInput.value })); const scope = 'offline_access Files.ReadWrite.All Sites.Read.All'; const redirectUri = window.location.origin + window.location.pathname; const authUrl = \`\${authEndpoints[cloudEnvSelect.value]}?client_id=\${clientId}&scope=\${scope}&response_type=code&redirect_uri=\${redirectUri}\`; window.location.href = authUrl; } async function handleGetToken(code) { const config = JSON.parse(localStorage.getItem('ms_graph_tool_config') || '{}'); if(!config.clientId || !config.clientSecret) { showResult('错误', '找不到本地存储的ID和密钥,请重新填写。', true); return; } spinner.classList.remove('hidden'); resultContainer.classList.remove('hidden'); resultContent.innerHTML = ''; try { const response = await fetch('./api/token', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ code: code, redirect_uri: window.location.origin + window.location.pathname, cloud_env: config.cloudEnv, client_id: config.clientId, client_secret: config.clientSecret }) }); const data = await response.json(); if (!response.ok) { throw new Error(JSON.stringify(data)); } appState.accessToken = data.access_token; appState.cloudEnv = config.cloudEnv; const resultHtml = \` <div class="space-y-4"> <div class="result-wrapper"><p class="font-semibold text-gray-700 dark:text-gray-300">刷新令牌 (Refresh Token):</p><div class="relative"><p class="text-sm p-3 bg-gray-100 dark:bg-gray-700 rounded break-all" id="refreshTokenText">\${data.refresh_token}</p><button class="copy-btn" onclick="copyToClipboard(document.getElementById('refreshTokenText').textContent, this)">复制</button></div></div> <div class="result-wrapper"><p class="font-semibold text-gray-700 dark:text-gray-300">访问令牌 (Access Token):</p><div class="relative"><p class="text-sm p-3 bg-gray-100 dark:bg-gray-700 rounded break-all" id="accessTokenText">\${data.access_token}</p><button class="copy-btn" onclick="copyToClipboard(document.getElementById('accessTokenText').textContent, this)">复制</button></div></div> </div>\`; showResult('令牌获取成功!', resultHtml); siteIdFinder.classList.remove('hidden'); } catch (error) { showResult('获取令牌失败', error.message, true); } } async function handleGetSiteId() { const fullUrl = sharepointUrlInput.value; if (!fullUrl) { alert('请输入 SharePoint 网站 URL'); return; } if (!appState.accessToken) { alert('访问令牌不可用。请先完成步骤1。'); return; } let hostname, sitePath; try { const urlObject = new URL(fullUrl); hostname = urlObject.hostname; sitePath = urlObject.pathname; } catch (e) { alert('输入的 SharePoint URL 格式无效。'); return; } const config = JSON.parse(localStorage.getItem('ms_graph_tool_config') || '{}'); config.sharepointUrl = fullUrl; localStorage.setItem('ms_graph_tool_config', JSON.stringify(config)); siteIdSpinner.classList.remove('hidden'); siteIdResultContainer.classList.remove('hidden'); siteIdResultContent.innerHTML = ''; try { const response = await fetch('./api/site-id', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ accessToken: appState.accessToken, cloudEnv: appState.cloudEnv, hostname: hostname, sitePath: sitePath }) }); const data = await response.json(); if (!response.ok) { throw new Error(JSON.stringify(data)); } const siteId = data.id; const resultHtml = \` <div class="result-wrapper"> <p class="font-semibold text-gray-700 dark:text-gray-300">网站信息:</p> <div class="relative text-sm p-3 bg-gray-100 dark:bg-gray-700 rounded break-all" id="siteIdText"> <p><strong>ID:</strong> \${siteId}</p> <p><strong>名称:</strong> \${data.displayName}</p> <p><strong>URL:</strong> <a href="\${data.webUrl}" target="_blank" class="text-blue-500 hover:underline">\${data.webUrl}</a></p> <button class="copy-btn" style="top: 1rem; transform: none;" onclick="copyToClipboard('\${siteId}', this)">复制ID</button> </div> </div> \`; showSiteIdResult('Site ID 获取成功!', resultHtml); } catch (error) { showSiteIdResult('获取 Site ID 失败', error.message, true); } } // --- Event Listeners & Page Load --- window.onload = () => { const savedConfig = JSON.parse(localStorage.getItem('ms_graph_tool_config') || '{}'); if (savedConfig.clientId) clientIdInput.value = savedConfig.clientId; if (savedConfig.clientSecret) clientSecretInput.value = savedConfig.clientSecret; if (savedConfig.cloudEnv) cloudEnvSelect.value = savedConfig.cloudEnv; if (savedConfig.sharepointUrl) sharepointUrlInput.value = savedConfig.sharepointUrl; const redirectUri = window.location.origin + window.location.pathname; redirectUriInput.value = redirectUri; const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); if (code) { handleGetToken(code); window.history.replaceState({}, document.title, window.location.pathname); } getAuthCodeBtn.addEventListener('click', handleStartAuth); getSiteIdBtn.addEventListener('click', handleGetSiteId); copyRedirectUriBtn.addEventListener('click', (e) => { e.preventDefault(); copyToClipboard(redirectUriInput.value, e.target); }); }; </script> </body> </html> `; } // --- Worker Main Logic & Router --- export default { async fetch(request) { const url = new URL(request.url); // Handle API routes if (url.pathname.startsWith('/api/')) { if (request.method === 'OPTIONS') { return handleOptions(); } if (url.pathname === '/api/token' && request.method === 'POST') { return handleTokenRequest(request); } if (url.pathname === '/api/site-id' && request.method === 'POST') { return handleSiteIdRequest(request); } } // Serve HTML for the root path if (url.pathname === '/' && request.method === 'GET') { return new Response(getHtmlContent(), { headers: { 'Content-Type': 'text/html;charset=utf-8' }, }); } return new Response('未找到', { status: 404 }); }, };
二、 创建应用
前提条件:注册 Azure 应用
https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps
- 在打开的页面,选择所在区域,点击创建应用
- 登陆后选择"注册应用程序",输入"名称",选择"任何组织目录中的账户和个人"(注意这里不要看位置选择而是看文字,部分人可能是中间那个选项,不要选成单一租户或者其他选项,否则会导致登陆时出现问题),输入重定向 URL 为 https://your-worker-name.your-subdomain.workers.dev ,点击注册即可,然后可以得到
client_id
- 注册好应用程序之后,选择"证书和密码",点击"新客户端密码",输入一串密码,选择时间为最长的那个,点击"添加"
(注:在添加之后输入的密码之后会消失,请记录下来client_secret
的值)
- 选择 "API 权限",点击 "Microsoft Graph",在"选择权限"中输入 file,勾选 Files.read(注:Files.read 是只读最小权限,图中权限较大,也同样可以),点击"确定"
三、操作步骤
获取令牌:
- 打开您部署好的 Worker URL。
- 在 步骤 1 的表单中,填入您的 应用 ID (Client ID) 和 客户端密码 (Client Secret)。
- 根据您的账号类型,选择正确的 云环境。
- 点击 开始认证 按钮,页面将跳转到微软登录页。
- 登录并授予应用权限。
- 页面会自动跳回,并显示获取成功的 刷新令牌 和 访问令牌。
获取 SharePoint Site ID:
- 令牌获取成功后,步骤 2 的卡片会自动出现。
- 在 SharePoint 网站 URL 输入框中,粘贴您需要查询的完整 SharePoint 网站地址(例如 https://contoso.sharepoint.com/sites/MyTeamSite)。
- 点击 获取 Site ID 按钮。
- 下方会显示查询到的网站信息,包括 Site ID、网站名称 和 URL。
⚠️ 安全警告
尽管此工具通过后端代理来处理 client_secret,但您仍然是在一个网页中输入您的凭据。请务必在您自己信任的电脑和网络环境下使用,并确保您部署的 Worker 是私密的。不要在公共场合或不安全的网络中使用。