Unique URL Every Time

时间:2015-12-18 作者:Eddie

我已经创建了一个小型搜索工具,除非你知道确切的代码,否则你不会被带到岗位上。例如,如果我输入“OI812”,它将带我到一个自定义帖子类型,即/CPT/OI812。

CPT不是常规搜索的一部分,我已经从规范重定向中删除了任何带有该段代码的内容。除非他们使用我的小型搜索工具并准确输入代码,否则它不会将他们带到页面。

到目前为止,一切顺利。但是,我希望每次他们访问页面时都有一个唯一生成的URL。E、 G/CPT/HASHorSOMETHINGcrazyANDrandom。这将使共享URL毫无用处,除非他们访问页面并将代码输入搜索工具。

我很好奇人们会怎么做?我到处搜索过,但像这样的搜索词似乎有点无处不在。感谢您的帮助。

3 个回复
SO网友:bosco

您可以使用JWTs 将包含标识信息和请求的post ID(或slug)的唯一令牌作为端点传递,并在验证时检查标识信息。

包含唯一标记的URL可以是rewritten 将令牌作为特定变量传递。A.\'parse_query\' action hook 然后可以检查是否存在令牌变量,并将查询替换为一个在令牌有效时将返回正确post的查询,或者在令牌无效时返回错误。

通过这种方式,只有发布令牌的访问者才能使用它访问帖子(除非其他人同时获取令牌并欺骗原始访问者的IP-这可以通过某种cookie或会话ID进一步保护)。没有你的秘密,伪造代币是不可能的。

$secret           = \'{insert randomly generated string here}\';
$custom_post_type = \'my_cpt\';
$unique_url_base  = \'cpt\';

add_action( \'init\', \'wpse_212309_rewrite_unique_token_url\' );

/**
 * Adds a rewrite rule to forward URLs in the format /cpt/{unique token} to
 * index.php?post_type=my_cpt&unique_token={unique token}. Supersedes WordPress
 * rewrites for the endpoint.
 **/
function wpse_212309_rewrite_unique_token_url(){
    add_rewrite_rule(
        trailingslashit( $unique_url_base ) . \'([^\\.]*.[^\\.]*)$\',
        \'index.php?post_type=\' . $custom_post_type . \'&unique_token=$matches[1]\',
        \'top\'
    );
}

add_action( \'parse_query\', \'wpse_212309_decode_unique_token_query\' );

/**
 * Replaces queries for the \'my_cpt\' post-type containing a unique token with
 * the appropriate \'my_cpt\' post if the token is valid (i.e., passed to the
 * server from the client IP to which it was assigned).
 **/
function wpse_212309_decode_unique_token_query( $wp ) {
    if( is_admin() )
        return;

    if( isset( $wp->query_vars[ \'p\' ] ) || $custom_post_type != $wp->query_vars[ \'post_type\' ] || empty( $_GET[ \'unique_token\' ] ) )
        return;

    $post_id = wpse_212309_get_post_id_from_unique_slug( $_GET[ \'unique_token\' ] );

    if( ! $post_id ) {
        $wp->set_404();
        status_header( 404 );
        return;
    }

    $wp->parse_request( \'p=\' . $post_id );
}

/**
 * Encodes data into a URL-friendly JWT-esque token including IP information
 * for the requesting party, as well as an optional expiration timestamp.
 **/
function wpse_212309_encode_token( $payload, $expiration = null ) {    
    $payload[ \'aud\' ] = hash( \'md5\', $_SERVER[ \'REMOTE_ADDR\' ] . $_SERVER[ \'HTTP_X_FORWARDED_FOR\' ] );
    $payload[ \'iss\' ] = time();

    if( isset( $expiration ) )
        $payload[ \'exp\' ] = $expiration;

    $payload = base64_encode( json_encode( $payload ) );
    $hash    = hash( \'md5\', $payload . $secret );

    return urlencode( $payload . \'.\' . $hash );
}

/**
 * Decodes a token generated by \'wpse_212309_encode_token()\', returning the
 * payload if the token is both unaltered and sent by the original client IP
 * or false otherwise.
 **/
function wpse_212309_decode_token( $token ) {
    if( empty( $token ) || -1 === strpos( $token, \'.\' ) )
        return false;

    $token   = urldecode( $token );
    $token   = explode( \'.\', $token );
    $hash    = $token[1];
    $payload = $token[0];

    // If the payload or the hash is missing, the token\'s invalid.
    if( empty( $payload ) || empty( $hash ) )
        return false;

    $hash_check = hash( \'md5\', $payload . $secret );

    // Has the payload and/or hash been modified since the token was issued?
    if( $hash_check !== $hash )
        return false;

    $payload = base64_decode( $payload );

    if( ! $payload )
        return false;

    $payload = json_decode( $payload, true );

    if( ! $payload )
        return false;

    $audience_check = hash( \'md5\', $_SERVER[ \'REMOTE_ADDR\' ] . $_SERVER[ \'HTTP_X_FORWARDED_FOR\' ] );

    // Was this token passed to the server by the IP that it was issued to?
    if( $audience_check != $payload[ \'aud\' ] )
        return false;

    // Does the payload have an expiration date - if so, has it expired?
    if( ! empty( $payload[ \'exp\' ] ) && $payload[ \'exp\' ] > time() )
        return false;

    // Token validated - return the payload as legitimate data.
    return $payload;
}

/**
 * Produces a token associating a post ID with a particular client, suitable
 * for inclusion in a URL. Optionally takes a "time to live" argument, in
 * in seconds, before the token should expire.
 **/
function wpse_212309_generate_unique_slug( $post_id, $ttl = null ) {
    $expiration = null;

    if( $ttl )
        $expiration = time() + $ttl * 1000;

    return wpse_212309_encode_token( array( \'pid\' => $post_id ), $expiration );
}

/**
 * Returns a post ID from a token if the token was in fact issued to the 
 * requesting client IP, or false otherwise.
 **/
function wpse_212309_get_post_id_from_unique_slug( $token ) {
    $payload = wpse_212309_decode_token( $token );

    if( ! $payload )
        return false;

    return $payload[ \'pid\' ];
}
您实际如何将访问者发送到包含令牌的唯一URL取决于您的应用程序(即您如何设置“搜索工具”),但使用上面的实现,您可以为帖子ID检索访问者唯一的slug,如下所示:

// A slug that only works for the visitor it was issued to:
$unique_slug = wpse_212309_generate_unique_slug( $post_id );

// OR, for one that additionally expires in an hour:
$unique_slug = wpse_212309_generate_unique_slug( $post_id, 3600 )

SO网友:jgraup

使用add_rewrite_ruleadd_rewrite_endpoint 要捕获HASHorSOMETHINGcrazyANDrandom.

Hashids 还可以帮助您生成一个哈希,稍后可以读取。

$hashids = new Hashids\\Hashids(\'this is my salt\');

$post_id = 1;
$request_id = 2;
$random = 3;

$crazy_id = $hashids->encode($post_id, $request_id, $random);

$numbers = $hashids->decode($crazy_id);
<小时>

UPDATE #1

这将创建两个端点:

http://example.com/CPT/{代码}/{哈希}

http://example.com/CPT/{代码}/

在这两种情况下,每次都会使用新链接生成一个新哈希。因为我正在使用wp_hash_password 我循环直到密码不包含/ 所以它不会破坏URL。我相信有更好的方法,但是。。。它适用于此测试。普通密码基于SERVER_NAME + {CODE} 这是从第一个参数中提取的。

每个哈希URL都是唯一的,但如果与正确的代码一起使用,则始终会进行验证。

<小时>

if( ! class_exists(\'HashPoint\')):

    class HashPoint {

        const ENDPOINT_NAME       = \'CPT\'; // endpoint to capture
        const ENDPOINT_QUERY_NAME = \'__cpt\'; // turns to param

        // WordPress hooks
        public function init() {
            add_filter(\'query_vars\', array($this, \'add_query_vars\'), 0);
            add_action(\'parse_request\', array($this, \'sniff_requests\'), 0);
            add_action(\'init\', array($this, \'add_endpoint\'), 0);
        }

        // Add public query vars
        public function add_query_vars($vars) {
            $vars[] = static::ENDPOINT_QUERY_NAME;
            $vars[] = \'code\';
            $vars[] = \'hash\';

            return $vars;
        }

        // Add API Endpoint
        public function add_endpoint() {
            add_rewrite_rule(\'^\' . static::ENDPOINT_NAME . \'/([^/]*)/([^/]*)/?\', \'index.php?\' . static::ENDPOINT_QUERY_NAME . \'=1&code=$matches[1]&hash=$matches[2]\', \'top\');
            add_rewrite_rule(\'^\' . static::ENDPOINT_NAME . \'/([^/]*)/?\', \'index.php?\' . static::ENDPOINT_QUERY_NAME . \'=1&code=$matches[1]\', \'top\');

            flush_rewrite_rules(false); //// <---------- REMOVE THIS WHEN DONE
        }

        // Sniff Requests
        public function sniff_requests($wp_query) {
            global $wp;

            if(isset($wp->query_vars[ static::ENDPOINT_QUERY_NAME ])) {
                $this->handle_request(); // handle it
            }
        }

        // Handle Requests
        protected function handle_request() {
            // Control the template used

            add_filter(\'template_include\', function($original_template) {

                // global $wp_query;
                // var_dump ( $wp_query->query_vars );

                // var_dump($original_template);

                return get_template_directory() . \'/crazy-hash.php\';
            });
        }
    }

    $hashEP = new HashPoint();
    $hashEP->init();

endif; // HashPoint
<?php
/**
 * Template Name: Crazy Hash
 */
get_header();

global $wp_query;
$code = $wp_query->query_vars[ \'code\' ];
$hash = empty($wp_query->query_vars[ \'hash\' ]) ? \'NONE\' : $wp_query->query_vars[ \'hash\' ];
$hash = urldecode($hash);

echo \'Code : \' . $code;
echo \'<br />\';
echo \'Hash : \' . $hash;
echo \'<br />\';
echo \'<br />\';

require_once(ABSPATH . \'wp-includes/class-phpass.php\');
$wp_hasher = new PasswordHash(8, true);

$plain = $_SERVER[ \'SERVER_NAME\' ] . \'-\' . $code;
$hash_mash = wp_hash_password($plain);

// make sure we don\'t have any `/` to break the url
while(strpos($hash_mash, \'/\')) {
    $hash_mash = wp_hash_password($plain);
}

echo \'Valid?<br />\';

if($wp_hasher->CheckPassword($plain, $hash)) {
    echo "YES, Matched<br /><br />";
}
else {
    echo "No, BAD HASH!!!<br /><br />";
}

$url = get_home_url(NULL, \'CPT/\' . $code . \'/\' . urlencode($hash_mash));

echo "Try this Hash : <a href=\\"$url\\">$hash_mash</a>";
echo \'<br /><br />\';

// ... more ...

get_footer();
 <小时>

UPDATE #2 | LIFETIME NONCE

对于@birgire-要获得一生的暂时性,您不需要删除wp_nonce_tick() 从…起wp_create_nonce?

function wp_create_lifetime_nonce($action = - 1) {
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if( ! $uid) { 
        $uid = apply_filters(\'lifetime_nonce_user_logged_out\', $uid, $action);
    }

    $token = wp_get_session_token();
    $i = 0;//wp_nonce_tick(); -- time is not a factor anymore

    return substr(wp_hash($i . \'|\' . $action . \'|\' . $uid . \'|\' . $token, \'nonce\'), - 12, 10);
}

function wp_verify_lifetime_nonce($nonce, $action = - 1) {
    $nonce = (string) $nonce;
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if( ! $uid) {
        $uid = apply_filters(\'lifetime_nonce_user_logged_out\', $uid, $action);
    }

    if(empty($nonce)) {
        return false;
    }

    $token = wp_get_session_token();
    $i = 0; //wp_nonce_tick();  -- time is not a factor anymore

    // Nonce generated anytime ago
    $expected = substr(wp_hash($i . \'|\' . $action . \'|\' . $uid . \'|\' . $token, \'nonce\'), - 12, 10);
    if(hash_equals($expected, $nonce)) {
        return 1;
    }

    do_action(\'wp_verify_lifetime_nonce_failed\', $nonce, $action, $user, $token);

    // Invalid nonce
    return false;
}
<小时>
$code = \'OI812\';

$lifetime_nonce = wp_create_lifetime_nonce($code);
$nonce = wp_create_nonce($code);

echo "<pre>";
print_r(
    array(
        $code,
        $lifetime_nonce,
        $nonce,
        ! wp_verify_nonce($nonce, $code) ? \'FAILED\' : \'WORKED\',
        ! wp_verify_lifetime_nonce($lifetime_nonce, $code) ? \'FAILED\' : \'WORKED\',
    ));
echo "</pre>";

SO网友:gmazzap

另一种简单的解决方案。

WordPress有一个内部系统来生成唯一的哈希值,然后可以对其进行验证:nonces. 通常使用nonce来防止CSRF attacks, 但考虑到开箱即用的可湿性粉剂:

  • 是一个哈希值,可以验证
  • 仅在有限的时间内有效
  • 耦合到特定的用户
    • 它非常适合您的所有需要。

      没有重写规则,老实说,我不喜欢WordPress重写API。规则存储在数据库中,在使用之前需要“刷新”,而且API本身也很不理想。

      在这种情况下,我只需要稍微低一点,然后使用\'do_parse_request\' 过滤钩子以检查机密url,并在需要时设置适当的变量。

      下面的代码的url格式我将假定如下链接:

      home_url( \'/s/\'. $post_id . \'|\' . wp_create_nonce(\'my-cpt\'.$post_id) )

      这使用了nonce,因此每个用户都是唯一的,即使是同一个用户也可以在有限的时间内使用它,默认情况下为1天,但可以使用\'nonce_life\' 滤器

      看起来像这样https://example.com/s/MTUy|ab5f9370de.

      使用描述url并发送到帖子\'do_parse_request\' 过滤器,我们可以截获像上面那样的url请求,然后由WordPres解析,我们可以阻止WordPress进一步处理url。

      is_admin() or add_filter(\'do_parse_request\', function($do, $wp) {
      
           // quick way to get current url
           $url = trim(esc_url_raw(add_query_arg(array())), \'/\');
           // check if WP has some subfolder in home url
           $path = trim(parse_url(home_url(),  PHP_URL_PATH), \'/\');
           $path and $path .= \'/\';
      
           // this is not one of ours secret urls, just do nothing
           if (strpos($url, $path.\'s/\') !== 0) {
               return $do;
      
           // extract post id and nonce from url
           $sectretUrl = explode(\'|\', preg_replace(\'~^\'.$path.\'s/~\', \'\', $url), 2);
           $id = (int) base64_decode(urldecode($sectretUrl[0])) ;
           $nonce = empty($sectretUrl[1]) ? false : $sectretUrl[1];
      
           // verify nonce, if not valid let WordPress continue the flow
           // that very likely ends on a 404
           if (!$id || !$nonce || ! wp_verify_nonce($nonce, \'my-cpt\'.$id)) {
               return $do;
      
           // everything ok, let\'s set query var and tell WP don\'t parse request
           $wp->query_vars = array(\'p\' => $id);
           $wp->is_secret_ok = $id;
           return false;
      
      }, PHP_INT_MAX, 2);
      
      就是这样,很有效。所有这些都只包含15行代码(不包括注释)的单个代码段。甚至不需要刷新重写规则。

      但你仍然有一个问题。

      禁用标准访问,https://example.com/s/MTUy|ab5f9370de, 很容易理解,第一部分是base64编码的内容。

      如果有人试图解码,将找到帖子id。并使用url,如"http://example.com?p={$decoded_id}" 此人将能够查看该帖子。

      此外,我想这篇文章的标题是显而易见的。如果您使用WordPress自动生成的slug,请使用如下url"http://example.com?p={$guessed_slug}" 您的帖子将再次可见。

      这意味着您需要阻止访问CPT帖子的标准url。可能只允许特权用户,如管理员和编辑器。

      add_action(\'template_redirect\', function() {
        // when not our CPT or user is privileged, do nothing
        if (! is_singular(\'my-cpt\') || current_user_can(\'edit_others_posts\')) {
          return;
        }
        global $wp;
        // if this is from "standard" url, exit with error
        if (! isset($wp->is_secret_ok) || $wp->is_secret_ok !== (int) get_queried_object_id()) {
           wp_die(\'Not allowed.\');
        }
        // prevent canonical redirect that will ends in a 404 request
        add_filter(\'redirect_canonical\', \'__return_false\');
      }, -1);
      
      为了防止访问来自“标准”url的用户,我使用了一个变量$wp->is_secret_ok 验证哈希url时,我在前面的代码段中设置的。

      创建机密的CPT链接我们需要的url有点复杂,所以我们可能需要创建一个函数来构建它,并将post ID作为param。

      也可以使用该函数过滤永久链接,并让WordPress在您调用时自动输出“机密”urlthe_permalink().

      类似这样:

      function my_cpt_secret_url($postId) {
         $id = urlencode(base64_encode((string)$postId));
      
         return home_url(\'/s/\'.$id.\'|\'.wp_create_nonce(\'my-cpt\'.$postId));
      }
      
      is_admin() or add_filter(\'post_type_link\', function($link, $post) {
        if ($post->post_type === \'my-cpt\') {
            return my_cpt_secret_url($post->ID)
        return $link;
      }, 30, 2);
      

相关推荐

Media searching ignored

我们的网站使用WordPress,有很多媒体文件。我们网站的媒体名称格式如下[Car brand\'s name]-[number].jpg, 例如Tesla-1.jpg 或Aston Martin-3.jpg. 因此,我们可以通过搜索文章的名称轻松找到文章的特定媒体。但突然间,我们找不到媒体。我们正在尝试搜索名称为的媒体,但搜索结果不变。(不搜索任何内容时的媒体屏幕)(搜索Aston Martin时的媒体屏幕)当然,在填充搜索文本框后,它会显示一个加载图标,但结果总是一样的。为什么会发生这种情况?更新+