PHP会话ID - 它们是如何生成的?

22
当我调用session_start()session_regenerate_id()时,PHP会生成一个看起来像是一个随机字符串的会话ID。我想知道的是,它只是一个随机字符序列,还是像uniqid()函数一样?
因为如果它只是随机字符,你理论上可能会遇到冲突。如果用户A登录,然后用户B登录,虽然很不可能,但用户B生成了相同的会话ID,那么用户B最终将访问用户A的帐户。
即使PHP检查是否已经存在具有相同ID的会话,并在这种情况下再次生成ID...我认为我不想要一个系统,它会生成相同ID两次,即使是在垃圾回收后--也许我想要存储一个表格,检查可能的劫持等。
如果不唯一,我该如何强制执行唯一性?我宁愿使用PHP配置来实现它,而不是在我编写的每个脚本中实现。关于PHP会话的好处是不必担心幕后的技术细节。

相关:https://dev59.com/5nVC5IYBdhLWcg3w9F89 - LiamB
1
如果您对安全性如此担忧,那么在提问之前应该先进行一些研究,因为有很多关于PHP会话安全和劫持的主题。对于安全环境和PCI合规等事项,原始状态下的会话是不可取的。 - Mark
1
128位或160位的有效随机性足以确保您在一生中永远不会看到碰撞 - 而且会话并不持续太久,因此问题甚至更加有限。 - hobbs
这并不是说您不应该忽略安全性,但使会话ID“真正独特”远远不是您可以做的最好的事情来提高安全性。 - hobbs
重点不在于唯一性作为安全约束。我只想存储会话ID的历史记录。在开始删除旧记录之前,我还不知道要存储多少个,但我不希望在我的数据库中出现冲突,因为它的主索引是会话ID。 - M Miller
我不认为这是一个重复的问题。这个问题询问PHP使用了什么算法,而另一个问题则是关于算法的唯一性/随机性的。 - Kayla
2个回答

56
如果您想了解PHP如何默认生成会话ID,请在Github上查看源代码。它肯定不是随机的,而是基于这些因素的哈希(默认:md5),请参见代码片段的310行:
  1. 客户端的IP地址
  2. 当前时间
  3. PHP线性同余生成器-伪随机数生成器(PRNG)
  4. 特定于操作系统的随机源-如果操作系统有可用的随机源(例如/dev/urandom)
如果操作系统有可用的随机源,则作为会话ID的生成强度很高(/dev/urandom和其他操作系统随机源通常是密码学安全的PRNG)。但是,如果没有,则仍然可以满足要求。
会话标识生成的目标是:
  1. 将生成两个具有相同值的会话ID的概率最小化
  2. 使计算随机密钥并击中正在使用的密钥变得非常具有挑战性。
PHP的会话生成方法实现了这一目标。
您不能绝对保证唯一性,但是命中相同哈希值的概率非常低,因此通常不值得担心。

1
感谢您提供完整的概述。我决定不必担心会话寿命相对较短。由于我还没有确定何时/多久截断数据库表,我将在主索引中添加时间戳以避免查询错误。另外,我将提高熵并将哈希函数设置为我的最爱——Whirlpool。再次感谢! - M Miller
没问题。是的,你可以更改散列函数为sha1或者在这个列表中的其他任何函数(http://www.php.net/manual/en/function.hash-algos.php)- 包括whirlpool! - GordyD
2
实际上,我们刚刚发现在两个不同的服务器后面运行反向代理并且它们的时钟同步时,生成相同会话ID的概率非常高。在这种情况下,REMOTE_ADDR对于2个服务器是相同的。而且,由于PRNG种子基于当前秒数,因此每个服务器平均获得150个会话ID中的2个相同的会话ID!我强烈建议始终确保会话熵设置为足够大(以防万一),并使用共享会话处理程序,如memcached或redis。 - Benoît Vidis
@BenoîtVidis 在一个唯一的服务器上,我希望PHP会检查唯一性;至于在2个或更多的服务器上,最好生成自己的算法(包括服务器编号在要哈希的字符串中),并强制PHP使用该哈希作为会话ID,以极大地减轻出现“已经看过”的风险 :) - Déjà vu

9
这里是生成id的代码: Session.c 具体来说,是 php_session_create_id 函数:
PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS) /* {{{ */
{
    PHP_MD5_CTX md5_context;
    PHP_SHA1_CTX sha1_context;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
    void *hash_context = NULL;
#endif
    unsigned char *digest;
    int digest_len;
    int j;
    char *buf, *outid;
    struct timeval tv;
    zval **array;
    zval **token;
    char *remote_addr = NULL;

    gettimeofday(&tv, NULL);

    if (zend_hash_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER"), (void **) &array) == SUCCESS &&
        Z_TYPE_PP(array) == IS_ARRAY &&
        zend_hash_find(Z_ARRVAL_PP(array), "REMOTE_ADDR", sizeof("REMOTE_ADDR"), (void **) &token) == SUCCESS
    ) {
        remote_addr = Z_STRVAL_PP(token);
    }

    /* maximum 15+19+19+10 bytes */
    spprintf(&buf, 0, "%.15s%ld%ld%0.8F", remote_addr ? remote_addr : "", tv.tv_sec, (long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);

    switch (PS(hash_func)) {
        case PS_HASH_FUNC_MD5:
            PHP_MD5Init(&md5_context);
            PHP_MD5Update(&md5_context, (unsigned char *) buf, strlen(buf));
            digest_len = 16;
            break;
        case PS_HASH_FUNC_SHA1:
            PHP_SHA1Init(&sha1_context);
            PHP_SHA1Update(&sha1_context, (unsigned char *) buf, strlen(buf));
            digest_len = 20;
            break;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
        case PS_HASH_FUNC_OTHER:
            if (!PS(hash_ops)) {
                php_error_docref(NULL TSRMLS_CC, E_ERROR, "Invalid session hash function");
                efree(buf);
                return NULL;
            }

            hash_context = emalloc(PS(hash_ops)->context_size);
            PS(hash_ops)->hash_init(hash_context);
            PS(hash_ops)->hash_update(hash_context, (unsigned char *) buf, strlen(buf));
            digest_len = PS(hash_ops)->digest_size;
            break;
#endif /* HAVE_HASH_EXT */
        default:
            php_error_docref(NULL TSRMLS_CC, E_ERROR, "Invalid session hash function");
            efree(buf);
            return NULL;
    }
    efree(buf);

    if (PS(entropy_length) > 0) {
#ifdef PHP_WIN32
        unsigned char rbuf[2048];
        size_t toread = PS(entropy_length);

        if (php_win32_get_random_bytes(rbuf, MIN(toread, sizeof(rbuf))) == SUCCESS){

            switch (PS(hash_func)) {
                case PS_HASH_FUNC_MD5:
                    PHP_MD5Update(&md5_context, rbuf, toread);
                    break;
                case PS_HASH_FUNC_SHA1:
                    PHP_SHA1Update(&sha1_context, rbuf, toread);
                    break;
# if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
                case PS_HASH_FUNC_OTHER:
                    PS(hash_ops)->hash_update(hash_context, rbuf, toread);
                    break;
# endif /* HAVE_HASH_EXT */
            }
        }
#else
        int fd;

        fd = VCWD_OPEN(PS(entropy_file), O_RDONLY);
        if (fd >= 0) {
            unsigned char rbuf[2048];
            int n;
            int to_read = PS(entropy_length);

            while (to_read > 0) {
                n = read(fd, rbuf, MIN(to_read, sizeof(rbuf)));
                if (n <= 0) break;

                switch (PS(hash_func)) {
                    case PS_HASH_FUNC_MD5:
                        PHP_MD5Update(&md5_context, rbuf, n);
                        break;
                    case PS_HASH_FUNC_SHA1:
                        PHP_SHA1Update(&sha1_context, rbuf, n);
                        break;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
                    case PS_HASH_FUNC_OTHER:
                        PS(hash_ops)->hash_update(hash_context, rbuf, n);
                        break;
#endif /* HAVE_HASH_EXT */
                }
                to_read -= n;
            }
            close(fd);
        }
#endif
    }

    digest = emalloc(digest_len + 1);
    switch (PS(hash_func)) {
        case PS_HASH_FUNC_MD5:
            PHP_MD5Final(digest, &md5_context);
            break;
        case PS_HASH_FUNC_SHA1:
            PHP_SHA1Final(digest, &sha1_context);
            break;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
        case PS_HASH_FUNC_OTHER:
            PS(hash_ops)->hash_final(digest, hash_context);
            efree(hash_context);
            break;
#endif /* HAVE_HASH_EXT */
    }

    if (PS(hash_bits_per_character) < 4
            || PS(hash_bits_per_character) > 6) {
        PS(hash_bits_per_character) = 4;

        php_error_docref(NULL TSRMLS_CC, E_WARNING, "The ini setting hash_bits_per_character is out of range (should be 4, 5, or 6) - using 4 for now");
    }

    outid = emalloc((size_t)((digest_len + 2) * ((8.0f / PS(hash_bits_per_character)) + 0.5)));
    j = (int) (bin_to_readable((char *)digest, digest_len, outid, (char)PS(hash_bits_per_character)) - outid);
    efree(digest);

    if (newlen) {
        *newlen = j;
    }

    return outid;
}

正如您所看到的,实际上的id是一些东西的混合物的哈希值,例如当天的时间。因此,有可能会遇到冲突,但是这种可能性非常低。事实上,除非您有大量并发用户,否则不值得担心。

不过,如果您真的很担心,可以通过设置不同的哈希算法session.hash_function来增加熵。

至于监视活动会话,这个问题已经很好地涵盖在了这篇文章中 Is it possible to see active sessions using php?

如果您在单台机器上使用单个php实例,则它实际上具有内置的会话管理器,它会在分配id之前检查该id是否已存在。但是,如果您运行多个实例或多台机器,则无法知道其他机器分配的id。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接