Cloudflare Workers 短链服务添加密码保护完整指南

概述

本文档介绍如何为 Cloudflare Workers 上的短链服务(URL Shortener)添加密码访问保护。实现以下功能:

  • 访问首页(根路径)需要密码
  • 访问具体短链(如 /abc123)不需要密码
  • 创建短链的 API 需要密码(防止滥用)

需要修改的文件

只需修改 1 个文件:Worker 的主入口文件(通常是 index.jsworker.js

完整代码

只添加账号密码访问

// 1. 定义默认跳转的外部 URL(当访问根目录或短码不存在时)
const DEFAULT_TARGET_URL = 'https://www.example.com'; 

// 2. 定义创建接口的路径
const CREATE_PATH = '/api/create';

// 统一的 JSON 响应工具
function jsonResponse(data, status = 200) {
  return new Response(JSON.stringify(data), {
    status: status,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

// 统一的重定向工具
function redirectResponse(url) {
  return new Response(null, {
    status: 302,
    headers: { 'Location': url },
  });
}

// ========== 添加认证函数 ==========
// 方案一:仅密码验证
function requireAuth(request, password) {
  const auth = request.headers.get('Authorization');
  if (!auth || !auth.startsWith('Basic ')) {
    return { authorized: false };
  }
  
  try {
    const base64 = auth.slice(6);
    const credentials = atob(base64);
    const [, inputPassword] = credentials.split(':');
    
    const encoder = new TextEncoder();
    const p1 = encoder.encode(inputPassword);
    const p2 = encoder.encode(password || '');
    
    if (p1.length !== p2.length) return { authorized: false };
    
    let valid = true;
    for (let i = 0; i < p1.length; i++) {
      if (p1[i] !== p2[i]) valid = false;
    }
    
    return { authorized: valid };
  } catch {
    return { authorized: false };
  }
}

// 方案二:账号+密码验证(推荐)
function requireAuthFull(request, env) {
  const auth = request.headers.get('Authorization');
  if (!auth || !auth.startsWith('Basic ')) {
    return { authorized: false };
  }
  
  try {
    const base64 = auth.slice(6);
    const credentials = atob(base64);
    const [inputUsername, inputPassword] = credentials.split(':');
    
    const validUsername = env.SHORTLINK_USERNAME || 'admin';
    const validPassword = env.SHORTLINK_PASSWORD || '';
    
    const usernameMatch = secureCompare(inputUsername, validUsername);
    const passwordMatch = secureCompare(inputPassword, validPassword);
    
    return { authorized: usernameMatch && passwordMatch };
  } catch {
    return { authorized: false };
  }
}

// 安全字符串比较
function secureCompare(a, b) {
  if (!a || !b) return a === b;
  const encoder = new TextEncoder();
  const aBuf = encoder.encode(a);
  const bBuf = encoder.encode(b);
  
  if (aBuf.length !== bBuf.length) return false;
  
  let result = 0;
  for (let i = 0; i < aBuf.length; i++) {
    result |= aBuf[i] ^ bBuf[i];
  }
  return result === 0;
}
// =================================

// 处理短链接创建/更新
async function handleCreate(request, env) {
  try {
    const body = await request.json();
    const { shortcode, url } = body;

    if (!shortcode || !url) {
      return jsonResponse({ success: false, message: 'Missing required fields: shortcode and url' }, 400);
    }
    
    try {
      new URL(url);
    } catch (e) {
      return jsonResponse({ success: false, message: 'Invalid URL format' }, 400);
    }

    const normalizedShortcode = shortcode.toLowerCase().trim();
    if (normalizedShortcode === '' || normalizedShortcode.startsWith('api')) {
      return jsonResponse({ success: false, message: 'Invalid or reserved shortcode' }, 400);
    }

    await env.LINKS.put(normalizedShortcode, url);

    const shortUrl = `${new URL(request.url).origin}/${normalizedShortcode}`;

    return jsonResponse({
      success: true,
      message: 'Short link created successfully',
      shortcode: normalizedShortcode,
      longUrl: url,
      shortUrl: shortUrl,
    }, 200);

  } catch (e) {
    return jsonResponse({ success: false, message: `Error processing request: ${e.message}` }, 500);
  }
}

// 核心逻辑调度器(添加了认证)
async function handleRequest(request, env) {
  const url = new URL(request.url);
  const path = url.pathname.slice(1);
  
  // ========== 添加 Basic Auth 认证 ==========
  // 选择使用方案一或方案二,二选一即可
  
  // 方案一:仅密码验证(需要设置环境变量 SHORTLINK_PASSWORD)
  const authResult = requireAuth(request, env.SHORTLINK_PASSWORD);
  
  // 方案二:账号+密码验证(需要设置环境变量 SHORTLINK_USERNAME 和 SHORTLINK_PASSWORD)
  // const authResult = requireAuthFull(request, env);
  
  if (!authResult.authorized) {
    return new Response("Unauthorized", { 
      status: 401,
      headers: { 
        'WWW-Authenticate': 'Basic realm="ShortLink Service"',
        'Access-Control-Allow-Origin': '*'
      }
    });
  }
  // ==========================================
  
  // 1. 拦截创建短链接的 POST 请求
  if (request.method === 'POST' && url.pathname === CREATE_PATH) {
    return handleCreate(request, env);
  }

  // 2. 拦截根路径 GET 请求 (/)
  if (request.method === 'GET' && path === '') {
    return redirectResponse(DEFAULT_TARGET_URL);
  }

  // 3. 处理短链接重定向逻辑
  if (request.method === 'GET') {
    const targetUrl = await env.LINKS.get(path); 

    if (targetUrl) {
      return redirectResponse(targetUrl);
    } else {
      return redirectResponse(DEFAULT_TARGET_URL); 
    }
  }

  return new Response('Method Not Allowed', { status: 405 });
}

// Cloudflare Workers 模块化标准入口
export default {
  async fetch(request, env, ctx) {
    return handleRequest(request, env);
  }
};

制作短链需要密码访问,短链不需要密码访问

// 1. 定义默认跳转的外部 URL(当访问根目录或短码不存在时)
const DEFAULT_TARGET_URL = 'https://www.example.com'; 

// 2. 定义创建接口的路径
const CREATE_PATH = '/api/create';

// 统一的 JSON 响应工具
function jsonResponse(data, status = 200) {
  return new Response(JSON.stringify(data), {
    status: status,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

// 统一的重定向工具
function redirectResponse(url) {
  return new Response(null, {
    status: 302,
    headers: { 'Location': url },
  });
}

// ========== 认证函数 ==========
// 方案一:仅密码验证
function requireAuth(request, password) {
  const auth = request.headers.get('Authorization');
  if (!auth || !auth.startsWith('Basic ')) {
    return { authorized: false };
  }
  
  try {
    const base64 = auth.slice(6);
    const credentials = atob(base64);
    const [, inputPassword] = credentials.split(':');
    
    const encoder = new TextEncoder();
    const p1 = encoder.encode(inputPassword);
    const p2 = encoder.encode(password || '');
    
    if (p1.length !== p2.length) return { authorized: false };
    
    let valid = true;
    for (let i = 0; i < p1.length; i++) {
      if (p1[i] !== p2[i]) valid = false;
    }
    
    return { authorized: valid };
  } catch {
    return { authorized: false };
  }
}

// 安全字符串比较函数
function secureCompare(a, b) {
  if (!a || !b) return a === b;
  const encoder = new TextEncoder();
  const aBuf = encoder.encode(a);
  const bBuf = encoder.encode(b);
  
  if (aBuf.length !== bBuf.length) return false;
  
  let result = 0;
  for (let i = 0; i < aBuf.length; i++) {
    result |= aBuf[i] ^ bBuf[i];
  }
  return result === 0;
}

// 方案二:账号+密码验证(可选)
function requireAuthFull(request, env) {
  const auth = request.headers.get('Authorization');
  if (!auth || !auth.startsWith('Basic ')) {
    return { authorized: false };
  }
  
  try {
    const base64 = auth.slice(6);
    const credentials = atob(base64);
    const [inputUsername, inputPassword] = credentials.split(':');
    
    const validUsername = env.SHORTLINK_USERNAME || 'admin';
    const validPassword = env.SHORTLINK_PASSWORD || '';
    
    const usernameMatch = secureCompare(inputUsername, validUsername);
    const passwordMatch = secureCompare(inputPassword, validPassword);
    
    return { authorized: usernameMatch && passwordMatch };
  } catch {
    return { authorized: false };
  }
}
// =================================

// 处理短链接创建/更新
async function handleCreate(request, env) {
  try {
    const body = await request.json();
    const { shortcode, url } = body;

    if (!shortcode || !url) {
      return jsonResponse({ success: false, message: 'Missing required fields: shortcode and url' }, 400);
    }
    
    try {
      new URL(url);
    } catch (e) {
      return jsonResponse({ success: false, message: 'Invalid URL format' }, 400);
    }

    const normalizedShortcode = shortcode.toLowerCase().trim();
    if (normalizedShortcode === '' || normalizedShortcode.startsWith('api')) {
      return jsonResponse({ success: false, message: 'Invalid or reserved shortcode' }, 400);
    }

    await env.LINKS.put(normalizedShortcode, url);

    const shortUrl = `${new URL(request.url).origin}/${normalizedShortcode}`;

    return jsonResponse({
      success: true,
      message: 'Short link created successfully',
      shortcode: normalizedShortcode,
      longUrl: url,
      shortUrl: shortUrl,
    }, 200);

  } catch (e) {
    return jsonResponse({ success: false, message: `Error processing request: ${e.message}` }, 500);
  }
}

// 核心逻辑调度器
async function handleRequest(request, env) {
  const url = new URL(request.url);
  const path = url.pathname.slice(1);      // 获取短码,例如 'abc123'
  const requestPath = url.pathname;         // 获取完整路径,例如 '/' 或 '/abc123'
  
  // 情况1:访问首页 (/) - 需要密码
  if (request.method === 'GET' && requestPath === '/') {
    // 默认使用仅密码验证
    const authResult = requireAuth(request, env.SHORTLINK_PASSWORD);
    // 如需使用账号+密码验证,注释上一行,取消下一行注释
    // const authResult = requireAuthFull(request, env);
    
    if (!authResult.authorized) {
      return new Response("Unauthorized", { 
        status: 401,
        headers: { 
          'WWW-Authenticate': 'Basic realm="ShortLink Service"',
          'Access-Control-Allow-Origin': '*'
        }
      });
    }
    // 认证成功,重定向到默认页面
    return redirectResponse(DEFAULT_TARGET_URL);
  }
  
  // 情况2:创建短链接 API - 需要密码(防止被滥用)
  if (request.method === 'POST' && url.pathname === CREATE_PATH) {
    const authResult = requireAuth(request, env.SHORTLINK_PASSWORD);
    // 如需使用账号+密码验证,注释上一行,取消下一行注释
    // const authResult = requireAuthFull(request, env);
    
    if (!authResult.authorized) {
      return new Response("Unauthorized", { 
        status: 401,
        headers: { 
          'WWW-Authenticate': 'Basic realm="ShortLink API"',
          'Access-Control-Allow-Origin': '*'
        }
      });
    }
    return handleCreate(request, env);
  }
  
  // 情况3:访问具体短链 (例如 /abc123) - 不需要密码
  if (request.method === 'GET') {
    const targetUrl = await env.LINKS.get(path);
    
    if (targetUrl) {
      // 找到对应的长链接,直接重定向,无需密码
      return redirectResponse(targetUrl);
    } else {
      // 短码不存在,重定向到首页(首页需要密码)
      return redirectResponse(DEFAULT_TARGET_URL);
    }
  }
  
  // 其他请求方法不允许
  return new Response('Method Not Allowed', { status: 405 });
}

// Cloudflare Workers 模块化标准入口
export default {
  async fetch(request, env, ctx) {
    return handleRequest(request, env);
  }
};

代码修改说明

认证添加位置

handleRequest 函数中,需要添加认证的位置有两处:

  1. 首页访问处理:在 if (request.method === 'GET' && requestPath === '/') 代码块内,在 return redirectResponse 之前添加认证
  2. API 创建处理:在 if (request.method === 'POST' && url.pathname === CREATE_PATH) 代码块内,在 return handleCreate 之前添加认证

如何切换认证方案

使用仅密码验证(默认)

const authResult = requireAuth(request, env.SHORTLINK_PASSWORD);

切换为账号+密码验证

将:

const authResult = requireAuth(request, env.SHORTLINK_PASSWORD);

改为:

const authResult = requireAuthFull(request, env);

环境变量设置

在 Cloudflare 仪表板中设置:

方案变量名类型
仅密码SHORTLINK_PASSWORD你的密码加密文本
账号+密码SHORTLINK_USERNAME你的用户名加密文本
账号+密码SHORTLINK_PASSWORD你的密码加密文本

设置步骤:

  1. 登录 Cloudflare Dashboard
  2. 进入 Workers 和 Pages → 你的 Worker
  3. 点击设置 → 变量
  4. 添加加密文本环境变量

访问效果

访问路径是否需要密码说明
https://你的域名/需要首页,输入密码后跳转到 DEFAULT_TARGET_URL
https://你的域名/abc123不需要具体短链,直接重定向到原链接
POST https://你的域名/api/create需要创建短链 API,防止他人滥用

API 调用示例

使用 curl 调用创建短链接接口:

# 仅密码验证
curl -X POST https://your-worker.dev/api/create \
  -H "Authorization: Basic $(echo -n ':mypassword' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"shortcode":"test","url":"https://example.com"}'

# 账号+密码验证
curl -X POST https://your-worker.dev/api/create \
  -H "Authorization: Basic $(echo -n 'admin:mypassword' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"shortcode":"test","url":"https://example.com"}'

部署命令

npx wrangler deploy

常见问题

Q: 修改代码后如何确认认证生效?

部署后,用浏览器无痕模式访问你的域名,应该会弹出登录框。

Q: 为什么具体短链不需要密码?

因为代码中只对 requestPath === '/' 的请求进行认证,其他路径直接放行。

Q: 如何保护创建 API 但公开首页?

只需删除首页认证的代码块,保留 API 认证即可。

Q: 如何修改默认跳转地址?

修改代码开头的 DEFAULT_TARGET_URL 变量值。

方案对比

特性方案一(仅密码)方案二(账号+密码)
需要修改的文件1 个1 个
需要设置的变量1 个2 个
用户名验证
安全性
实现复杂度简单简单

标签: none

添加新评论