搜索全站
建站 12 分钟阅读

素材站会员下载权限设计:免费与付费分层的实现方式

素材站怎么让免费用户看得到、付费用户下得走?本文对比三种会员权限实现路径,给出会员等级判断、每日下载限额、文件分发控制与订单打通的完整代码,以及常见越权漏洞的修复方法。

U
UFO_PF 2016-05-25 · 阅读 12 分钟
素材 建站
阅读提示
本文约 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 用户角色与权限。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。