loong博客

CDN 防盗链

目录

  • 基于Referer
  • 利用URL签名鉴权

基于Referer

HTTP协议规范在HTTP标头中定义了referer字段(RFC 7231),用于表示HTTP请求来源。该字段值由浏览器在发起HTTP请求时指定,代表当前HTTP请求的来源。例如在点击网页链接时,浏览器会向服务器提交一个HTTP请求,请求中HTTP标头的referer字段值为引用该资源的网页地址,即用户点击的网页地址。

  • 使用 AWS Lambda@Edge实现

某些应用场景下,用户需要配置复杂的访问控制规则。Lamba@Edge与CloudFront配合使用,可以有效应对复杂访问策略带来的挑战。Lambda@Edge是AWS利用Lambda无服务器计算服务结合CloudFront内容分发网络,在边缘站点运行Lambda代码,从而在边缘站点实现动态Web应用程序的技术。

由于Lambda@Edge通过编写代码实现对Web请求的精确过滤,因此可以为用户提供更加灵活的过滤条件和数据处理方式。Lambda@Edge功能支持使用Lambda在CloudFront边缘节点对HTTP请求和响应进行按需调整。当CloudFront收到用户请求,CloudFront从源端请求资源,CloudFront接收到源端反馈资源和CloudFront即将向用户返回资源时,均支持调用Lambda对HTTP请求或响应进行按需处理。

Lambda函数内容则如下图所示,用户可以在”handler”函数中编写自定义处理流程,在必要时,可以创建response对象,并将response对象的状态设置为403,从而达到禁止访问的效果。

20241104CDN基于Referer防盗链Lambda@Edge

  • 使用 Cloudflare Workers实现

Cloudflare Workers 是 Cloudflare 提供的无服务器服务,用于使无服务器功能尽可能接近最终用户运行。Cloudflare Workers 是根据 Service Workers API 用JavaScript 编写的,这意味着它们可以使用 Service Workers 提供的所有功能。区别在于 Service Workers 在客户端(Web 浏览器)内运行,而 Cloudflare Workers 在边缘服务器上运行。


const ALLOW_LIST = ["www.loongzxl.com"];

// Check requests for referer
const authorizeRefererRequest = (request) => {
    const referer = request.headers.get("referer");
    if (!referer) {
        return false;
    }
    const url = new URL(referer);
    const host = url.host;
    return ALLOW_LIST.includes(host);
};

export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        const key = url.pathname.slice(1);
        if (!authorizeRefererRequest(request)) {
            return new Response("Forbidden", { status: 403 });
        }
        switch (request.method) {
            case "GET":
                const object = await env.MY_BUCKET.get(key);
                if (object === null) {
                    return new Response("Object Not Found", { status: 404 });
                }
                const headers = new Headers();
                object.writeHttpMetadata(headers);
                headers.set("etag", object.httpEtag);
                return new Response(object.body, {
                    headers,
                });
            default:
                return new Response("Method Not Allowed", {
                    status: 405,
                    headers: {
                        Allow: "GET",
                    },
                });
        }
    },
};

利用URL签名鉴权

使用HTTP标头字段实现防盗链可以应对常见的盗链情形,但盗链者仍然可以通过其它手段去生成一个具有合法HTTP标头的请求,从而获取访问文件的能力。为了进一步提升文件访问的安全性,可以通过对请求的URL添加一个具有时效性的随机验证码作为签名。用户通过签名的地址访问相关资源,系统在后台对签名信息进行比对,确认签名正确性和时效性,从而识别当前请求是否有权访问对应文件。

  • 使用 AWS Lambda@Edge实现

AWS CloudFront Signed URL提供一整套签名管理方案,包括签名URL生成API,与CloudFront集成的签名验证机制,从而简化资源访问控制。

20241104CDN基于URL签名防盗链之一Lambda@Edge

客户端在访问CloudFront资源前,需要通过签名URL生成器获取经签名的URL地址。下面签名示例中签名URL主要包含几个要素:被签名的文件访问路径,签名URL生效和失效时间,请求客户端IP地址范围,以及签名URL使用的密钥信息。当CloudFront收到资源请求时,会自动识别URL中签名部分是否正确,是否仍在有效期内,从而确定是否返回对应资源。

20241104CDN基于URL签名防盗链之二Lambda@Edge

  • 使用 Cloudflare Workers实现

// How long an token should be valid for, in seconds
const EXPIRY = 5 * 60;

export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        const key = url.pathname.slice(1);
        const check = authorizeSignURLRequest(request, env)
        if (!check) {
            return new Response("Forbidden", { status: 403 });
        }
        if (check.code != 1) {
            return new Response(check.msg, { status: 403 });
        }
        switch (request.method) {
            case "GET":
                const object = await env.MY_BUCKET.get(key);
                if (object === null) {
                    return new Response("Object Not Found", { status: 404 });
                }
                const headers = new Headers();
                object.writeHttpMetadata(headers);
                headers.set("etag", object.httpEtag);
                return new Response(object.body, {
                    headers,
                });
            default:
                return new Response("Method Not Allowed", {
                    status: 405,
                    headers: {
                        Allow: "GET",
                    },
                });
        }
    },
};

// Check requests for a pre-shared secret
const authorizeSignURLRequest = (request, env) => {
    const url = new URL(request.url);
    // Make sure you have the necessary query parameters.
    const access_key_id = url.searchParams.get('key');
    if (!access_key_id) {
        return { code: 403, msg: "Invalid parameter:missing key" };
    }
    const timestamp = url.searchParams.get('timestamp');
    if (!timestamp) {
        return { code: 403, msg: "Invalid parameter:missing timestamp" };
    }
    const sign = url.searchParams.get('sign');
    if (!sign) {
        return { code: 403, msg: "Invalid parameter:missing sign" };
    }
    const access_key_secret = fetchAccessKeySecret(access_key_id, env);
    if (!access_key_secret) {
        return { code: 403, msg: "Invalid key" };
    }
    const assertedTimestamp = Number(timestamp);
    // Signed requests expire after five minute. Note that this value should depend on your specific use case
    if (Date.now() > assertedTimestamp + EXPIRY * 1000) {
        return { code: 403, msg: `URL expired at ${new Date(assertedTimestamp + EXPIRY * 1000)}` };
    }
    // md5(keysecret+path+timestamp)
    const dataToAuthenticate = `${access_key_secret}${url.pathname}${assertedTimestamp}`;
    const md5Data = createHash('md5').update(dataToAuthenticate).digest('hex');
    const verifiedData = Buffer.from(md5Data).toString("base64");
    if (verifiedData != sign) {
        return { code: 403, msg: "Invalid sign" };
    }
    return { code: 1, msg: "Valid url" };
};

// Get access key secret by access key id
const fetchAccessKeySecret = (key, env) => {
    var secret; 
    if (key === env.ACCESS_KEY_ID) {
        secret = env.ACCESS_KEY_SECRET;
    }
    return secret;
}

客户端在访问Cloudflare资源前,需要访问经签名的URL地址,上面示例中签名规则如下:

基于URL签名防盗链签名规则

参考