CF利用worker部署短域名系统

第一步:创建 KV 命名空间

登录 Cloudflare 后台,左侧找到 存储和数据库 → Workers KV,点击创建命名空间。取个名字,比如 shortlink,点击确定。

第二步:创建 Worker

左侧找到 计算 → Workers和Pages,点击 创建应用程序 → 从Hello World!开始,随便取一个名称,点击→部署

第三步:修改代码

进入刚创建的Workers,右上角→ 编辑代码,替换成下方代码,并保存部署

const config = {
  no_ref: "off", // 匿名链接控制:设置为 "on" 可隐藏 HTTP Referer 来源头
  cors: "on", // 允许 API 请求的跨域资源共享
  unique_link: true, // 唯一链接模式:若为 true,相同的长链接会始终生成同一个短链接后缀
  safe_browsing_api_key: "", // 谷歌安全浏览 API 密钥(留空则不开启安全检查)
  expiration_ttl: 0, // 短链接过期时间(秒),86400 = 24小时,设置为 0 表示永久有效
}

// ==================== 页面模板:404 未找到 ====================
const html404 = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>404 未找到 - 短链接系统</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: center; padding: 80px 20px; background: #f3f4f6; color: #333; }
    .card { background: white; max-width: 450px; margin: 0 auto; padding: 40px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
    h1 { font-size: 64px; color: #ef4444; margin: 0 0 10px 0; }
    p { font-size: 16px; color: #666; margin-bottom: 30px; }
    a { color: #667eea; text-decoration: none; font-weight: bold; }
    a:hover { text-decoration: underline; }
    .footer { margin-top: 40px; font-size: 13px; color: #999; border-top: 1px solid #eee; padding-top: 20px; }
  </style>
</head>
<body>
  <div class="card">
    <h1>404</h1>
    <p>抱歉,您访问的短链接不存在或已过期。</p>
    <p><a href="/">返回首页生成新链接</a></p>
    <div class="footer">
      Powered by Cloudflare Workers | <a href="https://www.cunzhangblog.com" target="_blank">Web3村长博客</a>
    </div>
  </div>
</body>
</html>`

// ==================== 页面模板:高颜值中文首页 ====================
const htmlIndex = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>极简短链接生成器</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 20px; color: #fff; }
    .container { background: rgba(255, 255, 255, 0.96); border-radius: 16px; padding: 40px 30px; width: 100%; max-width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); text-align: center; color: #333; margin: auto; }
    h1 { font-size: 26px; margin-bottom: 10px; color: #2d3748; }
    p.subtitle { color: #718096; font-size: 14px; margin-bottom: 30px; }
    .input-group { display: flex; flex-direction: column; gap: 15px; }
    input[type="url"] { width: 100%; padding: 14px; border: 2px solid #e2e8f0; border-radius: 8px; font-size: 15px; outline: none; transition: border-color 0.2s; }
    input[type="url"]:focus { border-color: #667eea; }
    button { width: 100%; padding: 14px; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: background 0.2s; }
    button:hover { background: #5a67d8; }
    .result-box { margin-top: 25px; padding: 15px; background: #f7fafc; border-radius: 8px; border: 1px dashed #cbd5e0; display: none; word-break: break-all; }
    .result-title { font-size: 13px; color: #4a5568; font-weight: bold; margin-bottom: 8px; }
    .result-url { font-size: 18px; color: #2b6cb0; margin-bottom: 12px; display: block; word-break: break-all; text-decoration: none; font-weight: 500; }
    .copy-btn { padding: 8px 16px; background: #48bb78; color: white; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; transition: background 0.2s; }
    .copy-btn:hover { background: #38a169; }
    footer { text-align: center; font-size: 13px; color: rgba(255,255,255,0.8); margin-top: 20px; }
    footer a { color: #fff; text-decoration: underline; font-weight: 500; }
  </style>
</head>
<body>
  <div class="container">
    <h1>极简短链接生成器</h1>
    <p class="subtitle">请输入要缩短的长链接(须包含 http:// 或 https://)</p>
    <div class="input-group">
      <input type="url" id="longUrl" placeholder="https://example.com" required>
      <button onclick="shortenUrl()">立即生成</button>
    </div>
    <div class="result-box" id="resultBox">
      <div class="result-title">🎉 短链接生成成功:</div>
      <a href="#" id="shortUrl" target="_blank" class="result-url"></a>
      <button class="copy-btn" id="copyBtn" onclick="copyToClipboard()">复制链接</button>
    </div>
  </div>
  <footer>
    <p>由 Cloudflare Workers 强力驱动 | <a href="https://www.cunzhangblog.com" target="_blank">Web3村长博客</a></p>
  </footer>
  <script>
    async function shortenUrl() {
      const longUrl = document.getElementById('longUrl').value.trim();
      if(!longUrl) { alert('请输入有效的网址!'); return; }
      if(!longUrl.startsWith('http://') && !longUrl.startsWith('https://')) {
         alert('网址格式错误!必须以 http:// 或 https:// 开头');
         return;
      }
      const btn = document.querySelector('button');
      btn.disabled = true;
      btn.innerText = '生成中...';
      try {
        const res = await fetch(window.location.pathname, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ url: longUrl })
        });
        const data = await res.json();
        if(res.ok && data.short_url) {
          const finalUrl = window.location.origin + data.short_url;
          const shortUrlLink = document.getElementById('shortUrl');
          shortUrlLink.href = finalUrl;
          shortUrlLink.innerText = finalUrl;
          document.getElementById('resultBox').style.display = 'block';
        } else {
          alert('生成失败: ' + (data.error || '未知错误'));
        }
      } catch(e) {
        alert('网络错误,请稍后重试');
      } finally {
        btn.disabled = false;
        btn.innerText = '立即生成';
      }
    }
    function copyToClipboard() {
      const urlText = document.getElementById('shortUrl').innerText;
      navigator.clipboard.writeText(urlText).then(() => {
        const copyBtn = document.getElementById('copyBtn');
        copyBtn.innerText = '复制成功!';
        copyBtn.style.background = '#38a169';
        setTimeout(() => {
          copyBtn.innerText = '复制链接';
          copyBtn.style.background = '#48bb78';
        }, 2000);
      }).catch(err => {
        alert('复制失败,请手动选择链接进行复制');
      });
    }
  </script>
</body>
</html>`

let response_header = {
  "content-type": "text/html;charset=UTF-8",
} 

if (config.cors == "on") {
  response_header = {
    "content-type": "application/json;charset=UTF-8",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  }
}

// 生成随机短链后缀
async function randomString(len) {
  len = len || 6;
  let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
  let maxPos = $chars.length;
  let result = '';
  for (let i = 0; i < len; i++) {
    result += $chars.charAt(Math.floor(Math.random() * maxPos));
  }
  return result;
}

// 计算 SHA-512(用于唯一链接匹配)
async function sha512(url) {
  url = new TextEncoder().encode(url)
  const url_digest = await crypto.subtle.digest({ name: "SHA-512" }, url)
  const hashArray = Array.from(new Uint8Array(url_digest));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// 检查 URL 格式
async function checkURL(URL) {
  let str = URL;
  let Expression = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
  let objExp = new RegExp(Expression);
  if (objExp.test(str) == true) {
    return str[0] == 'h';
  } else {
    return false;
  }
} 

function getKvPutOptions() {
  const MIN_TTL = 60;
  const rawTtl = Number(config.expiration_ttl);
  const hasValidTtl = Number.isFinite(rawTtl) && rawTtl >= MIN_TTL;
  return hasValidTtl ? { expirationTtl: Math.floor(rawTtl) } : {};
}

// 保存链接到 KV(带递归防冲突)
async function save_url(URL) {
  let random_key = await randomString()
  let is_exist = await LINKS.get(random_key)
  if (is_exist == null) {
    await LINKS.put(random_key, URL, getKvPutOptions());
    return random_key;
  } else {
    return save_url(URL);
  }
}

async function is_url_exist(url_sha512) {
  let is_exist = await LINKS.get(url_sha512)
  return is_exist || false;
}

// 谷歌安全浏览检查
async function is_url_safe(url) {
  let raw = JSON.stringify({
    "client": { "clientId": "Url-Shorten-Worker", "clientVersion": "1.0.7" },
    "threatInfo": {
      "threatTypes": ["MALWARE", "SOCIAL_ENGINEERING", "POTENTIALLY_HARMFUL_APPLICATION", "UNWANTED_SOFTWARE"],
      "platformTypes": ["ANY_PLATFORM"],
      "threatEntryTypes": ["URL"],
      "threatEntries": [{ "url": url }]
    }
  });

  let requestOptions = { method: 'POST', body: raw, redirect: 'follow' };
  try {
    let result = await fetch("https://safebrowsing.googleapis.com/v4/threatMatches:find?key=" + config.safe_browsing_api_key, requestOptions)
    result = await result.json()
    return Object.keys(result).length === 0;
  } catch (e) {
    return true; // 请求失败时默认放行,防止阻塞跳转
  }
}

// 核心请求处理器
async function handleRequest(request) {
  // 1. 处理跨域预检请求
  if (request.method === "OPTIONS") { 
    return new Response("", { headers: response_header })
  }

  // 2. 处理 POST 请求 - 创建短链接
  if (request.method === "POST") {
    let req = await request.json()
    
    if (!await checkURL(req["url"])) {
      return new Response(JSON.stringify({ status: 400, error: "网址格式不正确(必须包含 http:// 或 https://)" }), {
        headers: response_header,
        status: 400
      })
    }

    let random_key
    if (config.unique_link) {
      let url_sha512 = await sha512(req["url"])
      let url_key = await is_url_exist(url_sha512)
      if (url_key) {
        random_key = url_key
      } else {
        random_key = await save_url(req["url"])
        await LINKS.put(url_sha512, random_key, getKvPutOptions())
      }
    } else {
      random_key = await save_url(req["url"])
    }
    
    return new Response(JSON.stringify({
      status: 200,
      key: "/" + random_key,
      short_url: "/" + random_key
    }), { headers: response_header })
  }

  // 3. 处理 GET 请求 - 访问/跳转短链接
  const requestURL = new URL(request.url)
  const path = requestURL.pathname.split("/")[1]
  const params = requestURL.search

  // 如果访问的是根目录,直接展示内置的中文高颜值首页
  if (!path) {
    return new Response(htmlIndex, {
      headers: { "content-type": "text/html;charset=UTF-8" },
    })
  }

  // 从 KV 中获取目标长连接
  const value = await LINKS.get(path)
  let location = params ? value + params : value

  if (location) {
    // 安全浏览检查
    if (config.safe_browsing_api_key) {
      if (!(await is_url_safe(location))) {
        let warning_page = await fetch("https://xytom.github.io/Url-Shorten-Worker/safe-browsing.html")
        warning_page = await warning_page.text()
        warning_page = warning_page.replace(/{Replace}/gm, location)
        return new Response(warning_page, { headers: { "content-type": "text/html;charset=UTF-8" } })
      }
    }

    // 是否开启隐藏来源页重定向
    if (config.no_ref == "on") {
      let no_ref = await fetch("https://xytom.github.io/Url-Shorten-Worker/no-ref.html")
      no_ref = await no_ref.text()
      no_ref = no_ref.replace(/{Replace}/gm, location)
      return new Response(no_ref, { headers: { "content-type": "text/html;charset=UTF-8" } })
    } else {
      // 正常的 302 秒跳转
      return Response.redirect(location, 302)
    }
  }
  
  // 未找到对应的短链接,返回中文 404
  return new Response(html404, {
    headers: { "content-type": "text/html;charset=UTF-8" },
    status: 404
  })
}

addEventListener("fetch", async event => {
  event.respondWith(handleRequest(event.request))
})

 

第四步:绑定 KV 命名空间

在 Worker 页面下方找到 绑定 → 添加绑定 → KV 命名空间,点击添加绑定。

字段
变量名称LINKS
KV 命名空间选择刚才创建的命名空间

第五步:保存并部署

点击添加绑定后会自动部署。部署成功后即可使用 Cloudflare 分配的 workers.dev 域名访问。

第六步:绑定自定义域名(可选)

在 Worker 页面的域中,点击添加域名,绑定你自己的域名。

功能介绍

部署完成后,打开页面即可使用:

  • 输入长链接,点击生成,得到一个短链接
  • 复制短链接,浏览器打开自动跳转到原地址
  • 支持自定义短链(可进入KV空间里面设置)