本文约 4,666 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:会员系统上了,资源还是被白嫖
有站长给素材站加了会员功能,付费会员登录后页面会显示”下载”按钮,非会员看到”升级会员”。上线一周发现:分享群里流出了大量资源直链,非会员拿着链接一样能下。检查后发现,按钮虽然隐藏了,但文件真实地址是 wp-content/uploads/2026/03/xxx.zip,而且下载处理脚本只判断了”按钮是否显示”,根本没在服务端校验权限。
这是权限设计里最典型的错误:把权限控制做在”显示层”而不是”校验层”。前端隐藏按钮只是交互,真正的权限必须在服务端每一次请求时重新判断。资源站的权限设计还多一层复杂度:不仅要判断”是不是会员”,还要判断”今天还能下几个””这次下载的文件属于哪个等级””订单是否有效”。这些判断必须集中在一个入口,否则迟早出现遗漏。
原理分析:三种实现路径的取舍
第一条路是用成熟会员插件:Paid Memberships Pro、MemberPress、WooCommerce Memberships 等。它们提供会员等级、订阅、续费、内容保护(限制文章/分类访问)等能力,适合”卖会员”这种商业模式。
第二条路是电商式按件购买:Easy Digital Downloads(EDD)或 WooCommerce 的虚拟商品。适合”单个素材单独付费”,每个文件作为一个商品,订单中包含下载链接与次数限制。EDD 原生支持文件下载权限与下载次数限制,做素材站非常对口。
第三条路是自己写:用 WordPress 的角色与能力(current_user_can())+ 自定义字段记录会员等级与到期时间。灵活但不包含支付与续费,适合已有支付系统的团队。
| 方案 | 适合模式 | 权限粒度 | 维护成本 |
|---|---|---|---|
| Paid Memberships Pro | 会员订阅 | 按文章/分类 | 中 |
| MemberPress | 会员订阅 + 课程 | 细 | 中 |
| EDD | 单品购买 | 按文件 | 低 |
| 自研 | 已有支付体系 | 最细 | 高 |
无论选哪条路,服务端都要守住两道关:一是”能不能看到下载入口”,二是”拿到链接后能不能真的下载文件”。第二道关才是关键。
保姆级实操
步骤 1:定义会员等级与判断函数。 用一个统一的入口函数,避免判断逻辑散落各处。
function mimangfei_user_level( $user_id = 0 ) {
$user_id = $user_id ? $user_id : get_current_user_id();
if ( ! $user_id ) {
return 0;
}
$level = (int) get_user_meta( $user_id, '_member_level', true );
$expire = (int) get_user_meta( $user_id, '_member_expire', true );
// 到期自动降级为免费用户
if ( $level > 0 && $expire && $expire < time() ) {
update_user_meta( $user_id, '_member_level', 0 );
return 0;
}
return $level;
}
步骤 2:给资源设置所需等级,并在页面层控制显示。
function mimangfei_can_download( $post_id ) {
$need = (int) get_post_meta( $post_id, '_required_level', true );
return mimangfei_user_level() >= $need;
}
<?php if ( mimangfei_can_download( get_the_ID() ) ) : ?>
<a class="btn-download" href="<?php echo esc_url( mimangfei_download_url( get_the_ID() ) ); ?>">立即下载</a>
<?php else : ?>
<a class="btn-upgrade" href="/membership/">升级会员后可下载</a>
<?php endif; ?>
步骤 3:服务端二次校验——这是最关键的一步。 前端隐藏只是交互,下载接口必须独立判断。
add_action( 'template_redirect', function () {
if ( ! isset( $_GET['dl'] ) ) {
return;
}
$post_id = absint( $_GET['dl'] );
$nonce = isset( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '';
if ( ! wp_verify_nonce( $nonce, 'download_' . $post_id ) ) {
wp_die( '链接已过期,请刷新页面重试', '下载失败', array( 'response' => 403 ) );
}
if ( ! is_user_logged_in() ) {
auth_redirect();
}
if ( ! mimangfei_can_download( $post_id ) ) {
wp_die( '当前会员等级不足', '无法下载', array( 'response' => 403 ) );
}
// 校验通过后才进入文件分发逻辑
mimangfei_stream_file( $post_id );
exit;
} );
步骤 4:实现每日下载限额。 用 Transients API 计数,避免每次请求都写数据库。
function mimangfei_within_quota( $user_id, $limit = 10 ) {
$key = 'dl_quota_' . $user_id . '_' . gmdate( 'Ymd' );
$used = (int) get_transient( $key );
if ( $used >= $limit ) {
return false;
}
set_transient( $key, $used + 1, DAY_IN_SECONDS );
return true;
}
步骤 5:把文件放到 Web 目录之外。 即使权限逻辑有漏洞,攻击者也无法直接拼 URL 下载。
function mimangfei_stream_file( $post_id ) {
$path = get_post_meta( $post_id, '_protected_path', true );
$real = realpath( '/data/files/' . ltrim( $path, '/' ) );
// 防目录穿越
if ( ! $real || 0 !== strpos( $real, '/data/files/' ) || ! is_file( $real ) ) {
wp_die( '文件不存在', '下载失败', array( 'response' => 404 ) );
}
nocache_headers();
header( 'Content-Type: application/octet-stream' );
header( 'Content-Disposition: attachment; filename="' . rawurlencode( basename( $real ) ) . '"' );
header( 'Content-Length: ' . filesize( $real ) );
header( 'X-Accel-Redirect: /protected-files/' . ltrim( $path, '/' ) );
}
步骤 6:登录校验与权限缓存。 给会员判断加一层短缓存,减少高并发下的数据库压力。
function mimangfei_cached_level( $user_id ) {
$key = 'mlevel_' . $user_id;
$level = wp_cache_get( $key, 'membership' );
if ( false === $level ) {
$level = mimangfei_user_level( $user_id );
wp_cache_set( $key, $level, 'membership', 300 );
}
return $level;
}
常见踩坑
- 只隐藏下载按钮,不做服务端校验,知道 URL 就能下。
- 文件仍放在
wp-content/uploads/下,绕过一切权限逻辑。 - 会员到期时间只在登录时检查,用户保持登录状态就能一直用。
- 限额判断写在页面模板里,被缓存后完全失效。
- 权限判断逻辑分散在十几个文件,改一处漏一处。
- 案例:某模板站用”登录后显示下载按钮”的方式做权限,结果发现资源在盗版站上流通。复盘发现,付费用户把下载后的文件与真实 URL 一起分享,而非会员直接访问 URL 即可下载。修复方案是:文件全部移到 Web 根目录之外、下载链接带 30 分钟 nonce、为每个用户生成带水印的追踪 ID,泄露后可定位来源账号。
- 用
get_current_user_id()的结果直接判断,忽略了未登录时返回 0 的边界情况。 - 会员等级存在
user_meta但没有备份,数据库故障后全部清零。
总结
权限设计是否合格,用一个测试就能验证:退出登录,用曾经拿到的下载链接直接访问,如果还能下载,说明权限只做在了显示层。行动清单:① 所有文件放 Web 根目录之外;② 下载接口独立校验登录态与等级;③ 链接带 nonce 或签名;④ 到期时间在每次请求时校验;⑤ 限额用 Transients 而非页面计数;⑥ 权限判断集中到一个函数。资源站的其他环节可参考 资源站文件存储与流量成本、WordPress 资源下载站搭建 与 WordPress 用户角色与权限。
