搭建自己的防盗链随机图API (PHP)

发布于 2024-01-23  815 次阅读


AI 摘要

这篇文章介绍了如何搭建自己的防盗链随机图API,包括图床图片上传、基础随机图API的编写、访问限制措施(IP、域名等)、CDN设置(Referer和User-Agent防盗链),以及使用Token密钥控制资源访问时限。通过这些步骤,读者能够实现网站展示多样性和提升安全性。 😄
  • 字数6483
  • 阅读时长34 分钟

前言

本站首页以及文章封面使用了随机展示的方式,从图床的精选图库中随机抽取一张展示,让网站每次刷新都呈现出不一样的颜色。


本次笔记包含以下内容

  • 1.在图床中上传图片
  • 2.使用PHP搭建基础随机图API
  • 3.在CDN中配置防盗链措施

注:本站使用的OSS来自赞助商 又拍云 - 加速在线业务 - CDN加速 - 云存储 (upyun.com);PHP版本为8.1;三重防盗链措施(php一重,Referer 防盗链二重,Token三重)。


1.上传图片

准备好自己的图床,在图床中上传图片


i.选择图床

如果您有已经备案的域名,推荐使用 又拍云储存 (使用CDN/OSS需考虑防盗链)

没有域名可以使用 路过图床 (使用此类公共图床则无需考虑防盗链)


ii.上传图片

上传并记录图片的链接(URL),例如https://s11.ax1x.com/2024/01/23/pFZ7Qzt.jpg

如果您的图片链接不是规则的(如下图中的(1).webp,(2).webp那样),请使用a.文件记录的方式,如果是规则链接,请进入2.编写php程序


a.文件记录

将文件链接记录到img.txt的文件里(可以自己改名)


2.编写PHP程序

这个随机图的php需要有以下功能:

  • i.在图片链接中随机选择一个
  • ii.基础限制:限制访问次数,以及白名单
  • iii.进阶限制:与CDN Token 防盗链通信

您也可以根据需求选择需要的代码。


i.基础选择

//如果链接是不规则的,使用下面的代码
<?php
$arr=file('img.txt');//img.txt需要换成前文中存储不规则链接的文本文档
$n=count($arr)-1;
for ($i=1;$i<=1;$i++){
$x=rand(0,$n);header("Location:".$arr[$x],"\n");}
?>
//规则链接
<?php
$randomimg = mt_rand(1, 870);//870改成图床中最后一张图片的编号

$path = "/images/($randomimg).webp";//如果图床中图片编号没有括号,删除($randomimg)的括号
$fpath = "https://img.cmxz.top{$path}";//换成自己的链接

header("Location: $fpath");//重定向
exit();
?>

ii.限制访问

如果使用计费cdn,需要限制api调用次数,阻止盗刷流量。本文使用的是较为简易的限制,只能在一定程度上阻止盗刷。

限制原理:访问一次→ 记录标示符→ 访问次数+1 →如果达到上限→ 拒绝服务


a.php限制访问

以下代码均以 i.基础选择 的规则链接为前提

使用此代码务必新建allow.txt并将自己的域名记录到其中。

不在白名单中则限10张/5min ,白名单内的域名500张/5min (可自行修改)


//version 1.0
<?php
// 尝试获取客户端真实 IP 地址
$ip = $_SERVER['REMOTE_ADDR'];

// 如果 $_SERVER['REMOTE_ADDR'] 返回 0.0.0.0,则尝试使用 $_SERVER['HTTP_X_FORWARDED_FOR'] 或 $_SERVER['HTTP_X_REAL_IP']
if ($ip === '0.0.0.0') {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] !== '') {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_X_REAL_IP']) && $_SERVER['HTTP_X_REAL_IP'] !== '') {
        $ip = $_SERVER['HTTP_X_REAL_IP'];
    }
}

// 获取发起请求的域名
$callingDomain = '';
if (isset($_SERVER['HTTP_REFERER'])) {
    $refererParts = parse_url($_SERVER['HTTP_REFERER']);
    $callingDomain = isset($refererParts['host']) ? $refererParts['host'] : '';
}

// 生成唯一标识符,可以是 IP 和域名的组合
$identifier = $ip . '_' . $callingDomain;

// 设置每分钟允许的最大调用次数
$limit = 10; // 默认限制,可以根据实际需要调整

// 检查域名是否在 allow.txt 中
$allowedDomains = file('allow.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$isAllowed = in_array($callingDomain, $allowedDomains);

$limit = $isAllowed ? 500 : $limit;

$currentTimestamp = time();

// 读取记录文件,如果不存在则创建
$recordFile = 'records.txt';
if (!file_exists($recordFile)) {
    file_put_contents($recordFile, '');
}

$records = file_get_contents($recordFile);
$recordsArray = json_decode($records, true);

if (isset($recordsArray[$identifier])) {
    // 如果存在,检查时间戳是否在5分钟内
    if ($currentTimestamp - $recordsArray[$identifier]['timestamp'] < 300) {
        // 如果在5分钟内,检查调用次数是否达到限制
        if ($recordsArray[$identifier]['count'] >= $limit) {
            // 如果达到限制,返回 HTTP 403 Forbidden 错误
            header('HTTP/1.1 403 Forbidden');
            echo "访问太频繁,服务器要炸";
            exit();
        } else {
            // 如果在5分钟内,调用次数+1
            $recordsArray[$identifier]['count']++;
        }
    } else {
        // 如果不在5分钟内,重置时间戳和调用次数
        $recordsArray[$identifier] = array('timestamp' => $currentTimestamp, 'count' => 1);
    }
} else {
    // 如果标识符不存在,添加新的标识符
    $recordsArray[$identifier] = array('timestamp' => $currentTimestamp, 'count' => 1);
}

// 将更新后的记录写回文件
file_put_contents($recordFile, json_encode($recordsArray));

$time = date('Y-m-d H:i:s');
$counter = file_get_contents('counter.txt');
$counter = (int)$counter + 1;

// 获取访问的客户端类型
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';

// 获取当前域名
$domain = $callingDomain;

// 记录访问序号、IP地址、客户端类型、域名和访问时间到 recode.txt
file_put_contents('recode.txt', "Visit #$counter: IP: $ip, Calling Domain: $callingDomain, User Agent: $userAgent, Domain: $domain, Time: $time\n", FILE_APPEND);

// 更新计数器的值并写回 counter.txt
file_put_contents('counter.txt', $counter);

$randomimg = mt_rand(1, 870);//870改成图床中最后一张图片的编号

$path = "/images/($randomimg).webp";//如果图床中图片编号没有括号,删除($randomimg)的括号
$fpath = "https://img.cmxz.top{$path}";//换成自己的链接

header("Location: $fpath");//重定向
exit();
?>

此版本还存在的问题:

  • 不会清理过期的记录,需要手动清理,等待后续更新
  • ip限制能力有限,还需在cdn中进一步限制
  • 重定向后可以得到真实的图片链接

b.cdn限制

在cdn中进一步设置限制规则

比如 Referer 防盗链User-Agent 防盗链, Token防盗链在 c.Token防盗链 中另讲。

Referer 防盗链可以只允许白名单内的域名访问,允许为空便于自己调试

User-Agent 防盗链可以以防止Wp-scan之类的扫站

不同cdn服务商设置方法不同,本文不再赘述

c.Token防盗链

Token防盗链:通过设置 Token 密钥,配合签名过期时间来控制资源内容的访问时限,也即时间戳防盗链。(可以使资源链接定时过期),通过Token解决上一版本中“重定向后可以得到真实的图片链接”的问题,即便得到了真实链接也无法访问,只能通过api获得 15秒(可自行修改)内有效的链接

需要在CDN中配置Token密钥

本文提供包含 又拍云Token算法 的完整代码,即本站目前使用的api源码。

不过代码还存在些许不足,还请谅解

<?php
// 尝试获取客户端真实 IP 地址
$ip = $_SERVER['REMOTE_ADDR'];

// 如果 $_SERVER['REMOTE_ADDR'] 返回 0.0.0.0,则尝试使用 $_SERVER['HTTP_X_FORWARDED_FOR'] 或 $_SERVER['HTTP_X_REAL_IP']
if ($ip === '0.0.0.0') {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] !== '') {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_X_REAL_IP']) && $_SERVER['HTTP_X_REAL_IP'] !== '') {
        $ip = $_SERVER['HTTP_X_REAL_IP'];
    }
}

// 获取发起请求的域名
$callingDomain = '';
if (isset($_SERVER['HTTP_REFERER'])) {
    $refererParts = parse_url($_SERVER['HTTP_REFERER']);
    $callingDomain = isset($refererParts['host']) ? $refererParts['host'] : '';
}

// 生成唯一标识符,可以是 IP 和域名的组合
$identifier = $ip . '_' . $callingDomain;

// 设置每分钟允许的最大调用次数
$limit = 10; // 默认限制,可以根据实际需要调整

// 检查域名是否在 allow.txt 中
$allowedDomains = file('allow.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$isAllowed = in_array($callingDomain, $allowedDomains);

// 根据是否允许设置不同的限制
$limit = $isAllowed ? 500 : $limit;

// 获取当前时间戳
$currentTimestamp = time();

// 读取记录文件,如果不存在则创建
$recordFile = 'records.txt';
if (!file_exists($recordFile)) {
    file_put_contents($recordFile, '');
}

// 读取记录文件中的内容
$records = file_get_contents($recordFile);

// 将记录文件内容转换成数组
$recordsArray = json_decode($records, true);

// 检查当前标识符是否已经存在
if (isset($recordsArray[$identifier])) {
    // 如果存在,检查时间戳是否在5分钟内
    if ($currentTimestamp - $recordsArray[$identifier]['timestamp'] < 300) {
        // 如果在5分钟内,检查调用次数是否达到限制
        if ($recordsArray[$identifier]['count'] >= $limit) {
            // 如果达到限制,返回 HTTP 403 Forbidden 错误
            header('HTTP/1.1 403 Forbidden');
            echo "访问太频繁,服务器要炸";
            exit();
        } else {
            // 如果在5分钟内,调用次数+1
            $recordsArray[$identifier]['count']++;
        }
    } else {
        // 如果不在5分钟内,重置时间戳和调用次数
        $recordsArray[$identifier] = array('timestamp' => $currentTimestamp, 'count' => 1);
    }
} else {
    // 如果标识符不存在,添加新的标识符
    $recordsArray[$identifier] = array('timestamp' => $currentTimestamp, 'count' => 1);
}

// 将更新后的记录写回文件
file_put_contents($recordFile, json_encode($recordsArray));

// 获取当前时间
$time = date('Y-m-d H:i:s');

// 读取当前计数器的值
$counter = file_get_contents('counter.txt');
$counter = (int)$counter + 1;

// 获取访问的客户端类型
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';

// 获取当前域名
$domain = $callingDomain;

// 记录访问序号、IP地址、客户端类型、域名和访问时间到 recode.txt
file_put_contents('recode.txt', "Visit #$counter: IP: $ip, Calling Domain: $callingDomain, User Agent: $userAgent, Domain: $domain, Time: $time\n", FILE_APPEND);

// 更新计数器的值并写回 counter.txt
file_put_contents('counter.txt', $counter);

$randomimg = mt_rand(1, 870);

// 构建路径
$path = "/($randomimg).webp";

// 生成带有签名的链接
$randomSignedLink = generateSignedLink($path);

// 重定向到随机链接
header("Location: $randomSignedLink");
exit();

// 生成带有签名的链接的函数
function generateSignedLink($path) {
    // 防盗链参数配置
    $key = '换成自己的Token密钥'; 
    $expireTime = time() + 15; //过期时间为15秒

    // 构造签名
    $sign = substr( md5($key.'&'.$expireTime.'&'.$path), 12, 8 ) . $expireTime;
//取中间8位

    // 构造签名后的链接
    return "https://img.cmxz.top{$path}?_upt={$sign}";
}
?>