ChatGPT解决这个技术问题 Extra ChatGPT

“让我登录” - 最好的方法

我的 Web 应用程序使用会话在用户登录后存储有关用户的信息,并在用户在应用程序内从一个页面到另一个页面时维护该信息。在这个特定的应用程序中,我存储了此人的 user_idfirst_namelast_name

我想在登录时提供一个“让我保持登录”选项,它将在用户的机器上放置一个 cookie 两周,当他们返回应用程序时,它将以相同的详细信息重新启动他们的会话。

这样做的最佳方法是什么?我不想将他们的 user_id 存储在 cookie 中,因为这似乎会使一个用户很容易尝试伪造另一个用户的身份。


A
Adam

好吧,让我直截了当地说:如果您为此目的将用户数据或从用户数据派生的任何内容放入 cookie,那么您做错了什么。

那里。我说了。现在我们可以继续讨论实际的答案。

你问,散列用户数据有什么问题?嗯,它归结为暴露表面和通过默默无闻的安全性。

想象一下,你是一个攻击者。您会在会话中看到为 remember-me 设置的加密 cookie。它有 32 个字符宽。哎呀。那可能是MD5...

让我们再想象一下,他们知道您使用的算法。例如:

md5(salt+username+ip+salt)

现在,攻击者需要做的就是暴力破解“盐”(这实际上不是盐,但稍后会详细介绍),他现在可以使用他的 IP 地址的任何用户名生成他想要的所有假令牌!但是暴力破解盐很难,对吧?绝对地。但现代 GPU 非常擅长它。除非您在其中使用足够的随机性(使其足够大),否则它会迅速下降,并随之成为您城堡的钥匙。

简而言之,唯一能保护你的是盐,它并没有你想象的那样真正保护你。

可是等等!

所有这些都假设攻击者知道算法!如果它是秘密和令人困惑的,那么你是安全的,对吧?错误的。这种思路有一个名字:通过默默无闻的安全性,永远不应该依赖它。

更好的方式

更好的方法是永远不要让用户的信息离开服务器,除了 id。

当用户登录时,生成一个大的(128 到 256 位)随机令牌。将其添加到将令牌映射到用户 ID 的数据库表中,然后将其发送到 cookie 中的客户端。

如果攻击者猜到另一个用户的随机令牌怎么办?

好吧,让我们在这里做一些数学运算。我们正在生成一个 128 位随机令牌。这意味着有:

possibilities = 2^128
possibilities = 3.4 * 10^38

现在,为了说明这个数字有多大,让我们想象一下互联网上的每台服务器(假设今天有 50,000,000 台)试图以每台每秒 1,000,000,000 的速度暴力破解该数字。实际上,您的服务器会在这样的负载下融化,但让我们发挥一下。

guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000

所以每秒有 50 万亿次猜测。真快!正确的?

time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000

所以 6.8 六亿秒...

让我们尝试将其归结为更友好的数字。

215,626,585,489,599 years

甚至更好:

47917 times the age of the universe

是的,那是宇宙年龄的 47917 倍……

基本上,它不会被破解。

所以总结一下:

我推荐的更好的方法是将 cookie 存储为三个部分。

function onLogin($user) {
    $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
    storeTokenForUser($user, $token);
    $cookie = $user . ':' . $token;
    $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
    $cookie .= ':' . $mac;
    setcookie('rememberme', $cookie);
}

然后,验证:

function rememberMe() {
    $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
    if ($cookie) {
        list ($user, $token, $mac) = explode(':', $cookie);
        if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
            return false;
        }
        $usertoken = fetchTokenByUserName($user);
        if (hash_equals($usertoken, $token)) {
            logUserIn($user);
        }
    }
}

注意:不要使用令牌或用户和令牌的组合在数据库中查找记录。始终确保根据用户获取记录,然后使用时间安全比较功能来比较获取的令牌。 More about timing attacks

现在,SECRET_KEY 是一个密码机密(由类似 /dev/urandom 的东西生成和/或从高熵输入派生)是非常重要的。此外,GenerateRandomToken() 必须是强随机源(mt_rand() 还不够强。使用库,例如 RandomLibrandom_compat,或 mcrypt_create_iv()DEV_URANDOM)...

hash_equals() 是为了防止 timing attacks。如果您使用低于 PHP 5.6 的 PHP 版本,则不支持函数 hash_equals()。在这种情况下,您可以将 hash_equals() 替换为 timingSafeCompare 函数:

/**
 * A timing safe equals comparison
 *
 * To prevent leaking length information, it is important
 * that user input is always used as the second parameter.
 *
 * @param string $safe The internal (safe) value to be checked
 * @param string $user The user submitted (unsafe) value
 *
 * @return boolean True if the two strings are identical.
 */
function timingSafeCompare($safe, $user) {
    if (function_exists('hash_equals')) {
        return hash_equals($safe, $user); // PHP 5.6
    }
    // Prevent issues if string length is 0
    $safe .= chr(0);
    $user .= chr(0);

    // mbstring.func_overload can make strlen() return invalid numbers
    // when operating on raw binary strings; force an 8bit charset here:
    if (function_exists('mb_strlen')) {
        $safeLen = mb_strlen($safe, '8bit');
        $userLen = mb_strlen($user, '8bit');
    } else {
        $safeLen = strlen($safe);
        $userLen = strlen($user);
    }

    // Set the result to the difference between the lengths
    $result = $safeLen - $userLen;

    // Note that we ALWAYS iterate over the user-supplied length
    // This is to prevent leaking length information
    for ($i = 0; $i < $userLen; $i++) {
        // Using % here is a trick to prevent notices
        // It's safe, since if the lengths are different
        // $result is already non-0
        $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
    }

    // They are only identical strings if $result is exactly 0...
    return $result === 0;
}

但是这种方法是否意味着任何人都可以使用此用户名和 cookie 并以该用户身份从任何其他设备登录?
大声笑:-),请注意,47917 年是猜测的最长时间,随机令牌也可以在 1 小时内被猜测出来。
这很奇怪,因为您的代码与您的答案相矛盾。您说“如果您将用户数据放入 cookie [...] 您做错了什么”,但这正是您的代码正在做的事情!从 cookie 中删除用户名,仅计算令牌的哈希值(并可能添加 ip 地址以防止 cookie 被盗)然后在 rememberMe() 中执行 fetchUsernameByToken 而不是 fetchTokenByUserName 不是更好吗?
自 PHP 5.6 起,hash_equals 可用于在进行字符串比较时防止计时攻击。
这个答案中有很多好东西,但正如@levit 所指出的,cookie 内的散列是不必要的,因为用户和令牌都存储在数据库中。仅更改 cookie 中的用户 ID 不会帮助攻击者。数据库 needs to be hashed 中的令牌(它不在此处)以防止在数据库被盗的情况下滥用。 fetchTokenByUserName 是个坏主意,因为您可以从多台电脑、平板电脑、智能手机登录。最好检查令牌和用户名的组合是否在数据库中。
L
Lee Taylor

安全声明:基于确定性数据的 MD5 散列的 cookie 是一个坏主意;最好使用从 CSPRNG 派生的随机令牌。有关更安全的方法,请参阅 ircmaxell 对此问题的回答。

通常我会这样做:

用户使用“让我保持登录”登录 创建会话 创建一个名为 SOMETHING 的 cookie,其中包含:md5(salt+username+ip+salt) 和一个名为 somethingElse 的 cookie 包含 id 将 cookie 存储在数据库中 用户做事并离开 ---- 用户返回,检查 somethingElse cookie,如果存在,从数据库中为该用户获取旧哈希,检查 cookie 的内容 SOMETHING 与数据库中的哈希匹配,该哈希也应与新计算的哈希匹配(对于 ip ) 因此: cookieHash==databaseHash==md5(salt+username+ip+salt),如果有,转到 2,如果没有转到 1

当然,您可以使用不同的 cookie 名称等。您也可以稍微更改 cookie 的内容,只要确保它不容易创建即可。例如,您还可以在创建用户时创建一个 user_salt 并将其放入 cookie 中。

您也可以使用 sha1 而不是 md5 (或几乎任何算法)


为什么要在哈希中包含 IP?此外,请确保在 cookie 中包含时间戳信息,并使用此信息来确定 cookie 的最大年龄,这样您就不会创建对永恒有益的身份令牌。
@Abhishek Dilliwal:这是一个非常古老的线程,但我发现它正在寻找与 Mathew 相同的答案。我认为使用 session_ID 不适用于 Pim 的答案,因为您无法检查 db 哈希、cookie 哈希和当前 session_ID,因为 session_ID 每次 session_start() 都会更改;只是想我会指出这一点。
很抱歉我很无聊,但是第二个 cookie 的目的是什么?在这种情况下 id 是什么?是否只是一种简单的“真/假”值来指示用户是否想要使用让我保持登录功能?如果是这样,为什么不首先检查一下 cookie SOMETHING 是否存在?如果用户不希望他们的登录持续存在,那么 SOMETHING cookie 一开始就不会存在,对吧?最后,您是否再次动态生成哈希并将其与 cookie 和 DB 进行检查作为额外的安全措施?
令牌应该是随机的,不以任何方式与用户/他的 IP/他的用户代理/任何东西连接。这是主要的安全漏洞。
为什么要用两种盐? md5(salt+用户名+ip+salt)
Ö
Ömer An

介绍

你的标题“让我登录”——最好的方法让我很难知道从哪里开始,因为如果你正在寻找最好的方法,那么你必须考虑以下几点:

鉴别

安全

饼干

Cookie 很容易受到攻击,在常见的浏览器 cookie 盗窃漏洞和跨站点脚本攻击之间,我们必须承认 cookie 不安全。为了帮助提高安全性,您必须注意 php setcookies 具有其他功能,例如

bool setcookie ( 字符串 $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]])

安全(使用 HTTPS 连接)

httponly(通过 XSS 攻击减少身份盗用)

定义

令牌(长度为 n 的不可预测的随机字符串,例如 /dev/urandom)

参考(不可预测的 n 长度随机字符串,例如 /dev/urandom)

签名(使用 HMAC 方法生成键控哈希值)

简单的方法

一个简单的解决方案是:

用户使用记住我登录

带有令牌和签名的登录 Cookie

返回时,检查签名

如果签名没问题..然后在数据库中查找用户名和令牌

如果无效.. 返回登录页面

如果有效自动登录

上述案例研究总结了此页面上给出的所有示例,但它们的缺点是

无法知道 cookie 是否被盗

攻击者可能正在访问敏感操作,例如更改密码或个人和烘焙信息等数据。

被破坏的 cookie 在 cookie 的生命周期内仍然有效

更好的解决方案

更好的解决方案是

用户已登录并记住我已被选中

生成令牌和签名并存储在 cookie 中

令牌是随机的,仅对单个身份验证有效

每次访问该站点时都会替换该令牌

当未登录的用户访问该站点时,将验证签名、令牌和用户名

记住我的登录应该具有有限的访问权限,并且不允许修改密码、个人信息等。

示例代码

// Set privateKey
// This should be saved securely 
$key = 'fc4d57ed55a78de1a7b31e711866ef5a2848442349f52cd470008f6d30d47282';
$key = pack("H*", $key); // They key is used in binary form

// Am Using Memecahe as Sample Database
$db = new Memcache();
$db->addserver("127.0.0.1");

try {
    // Start Remember Me
    $rememberMe = new RememberMe($key);
    $rememberMe->setDB($db); // set example database

    // Check if remember me is present
    if ($data = $rememberMe->auth()) {
        printf("Returning User %s\n", $data['user']);

        // Limit Acces Level
        // Disable Change of password and private information etc

    } else {
        // Sample user
        $user = "baba";

        // Do normal login
        $rememberMe->remember($user);
        printf("New Account %s\n", $user);
    }
} catch (Exception $e) {
    printf("#Error  %s\n", $e->getMessage());
}

使用的类

class RememberMe {
    private $key = null;
    private $db;

    function __construct($privatekey) {
        $this->key = $privatekey;
    }

    public function setDB($db) {
        $this->db = $db;
    }

    public function auth() {

        // Check if remeber me cookie is present
        if (! isset($_COOKIE["auto"]) || empty($_COOKIE["auto"])) {
            return false;
        }

        // Decode cookie value
        if (! $cookie = @json_decode($_COOKIE["auto"], true)) {
            return false;
        }

        // Check all parameters
        if (! (isset($cookie['user']) || isset($cookie['token']) || isset($cookie['signature']))) {
            return false;
        }

        $var = $cookie['user'] . $cookie['token'];

        // Check Signature
        if (! $this->verify($var, $cookie['signature'])) {
            throw new Exception("Cokies has been tampared with");
        }

        // Check Database
        $info = $this->db->get($cookie['user']);
        if (! $info) {
            return false; // User must have deleted accout
        }

        // Check User Data
        if (! $info = json_decode($info, true)) {
            throw new Exception("User Data corrupted");
        }

        // Verify Token
        if ($info['token'] !== $cookie['token']) {
            throw new Exception("System Hijacked or User use another browser");
        }

        /**
         * Important
         * To make sure the cookie is always change
         * reset the Token information
         */

        $this->remember($info['user']);
        return $info;
    }

    public function remember($user) {
        $cookie = [
                "user" => $user,
                "token" => $this->getRand(64),
                "signature" => null
        ];
        $cookie['signature'] = $this->hash($cookie['user'] . $cookie['token']);
        $encoded = json_encode($cookie);

        // Add User to database
        $this->db->set($user, $encoded);

        /**
         * Set Cookies
         * In production enviroment Use
         * setcookie("auto", $encoded, time() + $expiration, "/~root/",
         * "example.com", 1, 1);
         */
        setcookie("auto", $encoded); // Sample
    }

    public function verify($data, $hash) {
        $rand = substr($hash, 0, 4);
        return $this->hash($data, $rand) === $hash;
    }

    private function hash($value, $rand = null) {
        $rand = $rand === null ? $this->getRand(4) : $rand;
        return $rand . bin2hex(hash_hmac('sha256', $value . $rand, $this->key, true));
    }

    private function getRand($length) {
        switch (true) {
            case function_exists("mcrypt_create_iv") :
                $r = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
                break;
            case function_exists("openssl_random_pseudo_bytes") :
                $r = openssl_random_pseudo_bytes($length);
                break;
            case is_readable('/dev/urandom') : // deceze
                $r = file_get_contents('/dev/urandom', false, null, 0, $length);
                break;
            default :
                $i = 0;
                $r = "";
                while($i ++ < $length) {
                    $r .= chr(mt_rand(0, 255));
                }
                break;
        }
        return substr(bin2hex($r), 0, $length);
    }
}

在 Firefox 和 Chrome 中测试

https://i.stack.imgur.com/9HiEL.png

优势

更好的安全性

攻击者访问受限

当 cookie 被盗时,它仅对单次访问有效

当下一次原始用户访问该站点时,您可以自动检测并通知用户盗窃

坏处

不支持通过多个浏览器(移动和网络)的持久连接

cookie 仍然可能被盗,因为用户只有在下次登录后才会收到通知。

快速解决

为每个必须具有持久连接的系统引入审批系统

使用多个 cookie 进行身份验证

多 Cookie 方法

当攻击者要窃取 cookie 时,只需将其集中在特定网站或域上,例如。例子.com

但实际上,您可以对来自 2 个不同域(example.com 和 fakeaddsite.com)的用户进行身份验证,并使其看起来像“广告 Cookie”

用户登录 example.com 并记住我

将用户名、令牌、引用存储在 cookie 中

将用户名、令牌、引用存储在数据库中,例如。内存缓存

通过 get 和 iframe 将参考 ID 发送到 fakeaddsite.com

fakeaddsite.com 使用引用从数据库中获取用户和令牌

fakeaddsite.com 存储签名

当用户从 fakeaddsite.com 返回使用 iframe 获取签名信息时

合并数据并进行验证

.....你知道剩下的

有些人可能想知道如何使用 2 个不同的 cookie?有可能,想象一下 example.com = localhostfakeaddsite.com = 192.168.1.120。如果您检查 cookie,它看起来像这样

https://i.stack.imgur.com/JflH3.png

从上图

当前访问的站点是 localhost

它还包含从 192.168.1.120 设置的 cookie

192.168.1.120

只接受定义的 HTTP_REFERER

只接受来自指定 REMOTE_ADDR 的连接

无 JavaScript,无内容,但仅包含签名信息并从 cookie 添加或检索它

优势

99% 的时间你都欺骗了攻击者

您可以在攻击者第一次尝试时轻松锁定帐户

与其他方法一样,即使在下次登录之前也可以防止攻击

坏处

对服务器的多个请求仅用于一次登录

改进

完成使用 iframe 使用 ajax


尽管@ircmaxell 很好地描述了该理论,但我更喜欢这种方法,因为它无需存储用户 ID(这将是不必要的披露)就可以很好地工作,并且还包括更多的指纹,而不仅仅是用户 ID 和哈希来识别用户,例如浏览器。这使得攻击者更难利用被盗的 cookie。这是迄今为止我见过的最好和最安全的方法。 +1
u
user253780

旧线程,但仍然是一个有效的问题。我注意到一些关于安全性的良好反应,并避免使用“通过默默无闻的安全性”,但在我看来,给出的实际技术方法还不够。在我贡献我的方法之前我必须说的事情:

永远不要以明文形式存储密码......永远!

切勿将用户的散列密码存储在数据库中的多个位置。您的服务器后端始终能够从用户表中提取散列密码。存储冗余数据代替额外的数据库事务并不是更有效,反之亦然。

您的会话 ID 应该是唯一的,因此没有两个用户可以共享一个 ID,因此 ID 的用途(您的驾驶执照 ID 号码可以匹配另一个人吗?不。)这会生成一个两部分的唯一组合,基于 2唯一的字符串。您的 Sessions 表应该使用 ID 作为 PK。要允许多个设备被信任以进行自动登录,请为受信任的设备使用另一个表,其中包含所有经过验证的设备的列表(请参见下面的示例),并使用用户名进行映射。

将已知数据散列到 cookie 中没有任何作用,cookie 可以被复制。我们正在寻找的是一种合规的用户设备,以提供在攻击者不破坏用户机器的情况下无法获得的真实信息(再次参见我的示例)。然而,这将意味着,禁止其机器的静态信息(即 MAC 地址、设备主机名、用户代理,如果受浏览器限制等)保持一致(或首先对其进行欺骗)的合法用户将无法使用此功能。但如果这是一个问题,请考虑这样一个事实,即您为唯一标识自己的用户提供自动登录,所以如果他们拒绝通过欺骗他们的 MAC、欺骗他们的用户代理、欺骗/更改他们的主机名、隐藏在代理后面来被人知道,等等,那么它们是不可识别的,并且永远不应该为自动服务进行身份验证。如果您想要这样做,您需要研究与客户端软件捆绑在一起的智能卡访问,该软件为正在使用的设备建立身份。

话虽如此,有两种很好的方法可以在您的系统上进行自动登录。

首先,将一切都交给别人的廉价、简单的方法。如果您让您的网站支持使用您的 google+ 帐户登录,那么您可能有一个简化的 google+ 按钮,如果用户已经登录到 google,则该按钮将使用户登录(我这样做是为了回答这个问题,因为我总是登录谷歌)。如果您希望用户在已使用受信任和受支持的身份验证器登录的情况下自动登录,并选中该框以执行此操作,请让您的客户端脚本在加载之前执行相应“登录方式”按钮后面的代码,只需确保让服务器在自动登录表中存储一个唯一 ID,该表具有用户名、会话 ID 和用于用户的身份验证器。由于这些登录方法使用 AJAX,因此无论如何您都在等待响应,而该响应要么是经过验证的响应,要么是拒绝响应。如果您收到经过验证的响应,请照常使用它,然后照常继续加载登录用户。否则,登录失败,但不告诉用户,继续,因为没有登录,他们会注意到。这是为了防止窃取 cookie(或伪造它们以试图提升权限)的攻击者获悉用户自动登录站点。

这很便宜,也可能被一些人认为是肮脏的,因为它试图验证你可能已经在谷歌和 Facebook 等地方登录的自己,甚至没有告诉你。但是,它不应该用于未要求自动登录您的网站的用户,并且此特定方法仅用于外部身份验证,例如 Google+ 或 FB。

由于使用外部身份验证器在幕后告诉服务器用户是否已通过验证,因此攻击者无法获得除唯一 ID 之外的任何内容,而唯一 ID 本身是无用的。我会详细说明:

用户“joe”第一次访问站点,会话 ID 放置在 cookie“会话”中。

用户“joe”登录、提升权限、获取新会话 ID 并更新 cookie“会话”。

用户 'joe' 选择使用 google+ 自动登录,获得放置在 cookie 'keepmesignedin' 中的唯一 ID。

用户“乔”让谷歌让他们保持登录状态,从而允许您的网站在您的后端使用谷歌自动登录用户。

攻击者系统地尝试为“keepmesignedin”(这是分发给每个用户的公共知识)的唯一 ID,并且没有登录到其他任何地方;尝试赋予“joe”的唯一 ID。

服务器收到“joe”的唯一 ID,在数据库中为 google+ 帐户提取匹配项。

服务器将攻击者发送到登录页面,该页面运行 AJAX 请求到谷歌登录。

谷歌服务器收到请求,使用它的 API 来查看攻击者当前没有登录。

Google 通过此连接发送响应,指出当前没有登录用户。

攻击者的页面收到响应,脚本会自动重定向到登录页面,并在 url 中编码一个 POST 值。

登录页面获取 POST 值,将 'keepmesignedin' 的 cookie 发送到一个空值和 1970 年 1 月 1 日的有效截止日期,以阻止自动尝试,导致攻击者的浏览器简单地删除 cookie。

攻击者获得正常的首次登录页面。

无论如何,即使攻击者使用不存在的 ID,尝试在所有尝试中都应该失败,除非收到经过验证的响应。

对于使用外部身份验证器登录您网站的用户,此方法可以而且应该与您的内部身份验证器结合使用。

=========

现在,对于您自己的可以自动登录用户的身份验证器系统,我是这样做的:

DB有几个表:

TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...

请注意,用户名的长度可以为 255 个字符。我的服务器程序将系统中的用户名限制为 32 个字符,但外部身份验证器的用户名可能具有 @domain.tld 大于此值,因此我只支持电子邮件地址的最大长度以实现最大兼容性。

TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL

注意这个表中没有用户字段,因为用户名,登录时在会话数据中,程序不允许空数据。 session_id 和 session_token 可以使用随机 md5 散列、sha1/128/256 散列、添加随机字符串然后散列的日期时间戳或您想要的任何内容生成,但输出的熵应保持在可容忍的最高水平减轻蛮力攻击甚至启动,并且在尝试添加会话类之前,应检查会话表中的所有哈希值是否匹配。

TABLE autologin:
UID - auto increment, PK
username - varchar(255), NOT NULL, allow duplicates
hostname - varchar(255), NOT NULL, allow duplicates
mac_address - char(23), NOT NULL, unique
token - varchar(?), NOT NULL, allow duplicates
expires - datetime code

MAC 地址本质上应该是唯一的,因此每个条目都有一个唯一的值是有意义的。另一方面,主机名可以合法地在不同的网络上复制。有多少人使用“Home-PC”作为他们的计算机名称之一?用户名是由服务器后端从会话数据中获取的,因此无法对其进行操作。至于令牌,应该使用为页面生成会话令牌的相同方法在 cookie 中为用户自动登录生成令牌。最后,添加日期时间代码用于用户需要重新验证其凭据的时间。要么在用户登录时更新此日期时间,将其保留在几天内,要么强制其过期,无论上次登录如何,仅保留一个月左右,无论您的设计如何。

这可以防止有人系统地欺骗他们知道自动登录的用户的 MAC 和主机名。永远不要让用户使用密码、明文或其他方式保存 cookie。就像会话令牌一样,在每个页面导航上重新生成令牌。这大大降低了攻击者获取有效令牌 cookie 并使用它登录的可能性。有些人会尝试说攻击者可以从受害者那里窃取 cookie 并进行会话重放攻击以登录。如果攻击者可以窃取 cookie(这是可能的),他们肯定会破坏整个设备,这意味着他们无论如何都可以使用该设备登录,这完全违背了窃取 cookie 的目的。只要您的网站通过 HTTPS 运行(在处理密码、CC 号码或其他登录系统时应该这样做),您就可以在浏览器中为用户提供所有保护。

要记住的一件事:如果您使用自动登录,会话数据不应过期。您可以错误地终止继续会话的能力,但如果会话数据是预期在会话之间继续的持久数据,则验证进入系统应该恢复会话数据。如果您想要持久性和非持久性会话数据,请使用另一个表来存储用户名作为 PK 的持久性会话数据,并让服务器像普通会话数据一样检索它,只需使用另一个变量。

以这种方式登录后,服务器仍应验证会话。在这里,您可以对被盗或受损系统的预期进行编码;登录会话数据的模式和其他预期结果通常会导致系统被劫持或 cookie 被伪造以获取访问权限的结论。这是您的 ISS 技术人员可以设置规则的地方,这些规则会触发帐户锁定或从自动登录系统中自动删除用户,从而使攻击者远离足够长的时间,以便用户确定攻击者如何成功以及如何将其切断。

最后,请确保任何超过阈值的恢复尝试、密码更改或登录失败都会导致自动登录被禁用,直到用户正确验证并确认已发生这种情况。

如果有人希望在我的回答中给出代码,我深表歉意,这不会在这里发生。我会说我使用 PHP、jQuery 和 AJAX 来运行我的网站,而且我从不使用 Windows 作为服务器......永远。


C
Community

我从一个角度问了这个问题 here,答案将引导您找到您需要的所有基于令牌的超时 cookie 链接。

基本上,您不会将 userId 存储在 cookie 中。您存储一个一次性令牌(巨大的字符串),用户使用它来获取他们的旧登录会话。然后为了使其真正安全,您要求输入密码以进行繁重的操作(例如更改密码本身)。


W
Walter Rumsby

我会推荐 Stefan 提到的方法(即遵循 Improved Persistent Login Cookie Best Practice 中的指南)并且还建议您确保您的 cookie 是 HttpOnly cookies,这样它们就不会被潜在的恶意 JavaScript 访问。


J
Jani Hartikainen

生成一个哈希,可能只有您知道的秘密,然后将其存储在您的数据库中,以便它可以与用户相关联。应该工作得很好。


这会是创建用户时创建的唯一标识符,还是会在每次用户生成新的“让我登录”cookie 时更改?
Tim Jansson 的回答描述了一种生成哈希的好方法,尽管如果它不包含密码我会觉得更安全
E
Enigma Plus

我不明白将加密内容存储在 cookie 中的概念,因为它是您需要进行黑客攻击的加密版本。如果我遗漏了什么,请发表评论。

我正在考虑采用这种方法来“记住我”。如果您看到任何问题,请发表评论。

创建一个表来存储“记住我”数据 - 与用户表分开,以便我可以从多个设备登录。成功登录后(勾选记住我): a) 生成一个唯一的随机字符串,用作这台机器上的用户 ID:bigUserID b) 生成一个唯一的随机字符串:bigKey c) 存储一个 cookie:bigUserID:bigKey d) 在“记住我”表,添加一条记录:用户 ID、IP 地址、bigUserID、bigKey 如果尝试访问需要登录的内容:a) 检查 cookie 并搜索具有匹配 IP 地址的 bigUserID 和 bigKey b) 如果找到它,登录该人,但在用户表“软登录”中设置一个标志,以便对于任何危险操作,您可以提示完全登录。注销时,将该用户的所有“记住我”记录标记为已过期。

我能看到的唯一漏洞是;

您可以获取某人的笔记本电脑并使用 cookie 欺骗他们的 IP 地址。

您可以每次欺骗一个不同的 IP 地址并猜测整个事情 - 但是要匹配两个大字符串,那就是......进行与上述类似的计算......我不知道......巨大的几率?


您好,感谢您的回答,我喜欢它。不过有一个问题:为什么必须生成 2 个随机字符串 - bigUserID 和 bigKey?你为什么不只生成 1 并使用它?
bigKey 在预定义的时间后过期,但 bigUserID 不会。 bigUserID 是允许您在同一 IP 地址的不同设备上进行多个会话。希望这是有道理的——我不得不考虑片刻:)
拥有 hmac 可以提供帮助的一件事是,如果您发现 hmac 被篡改,您肯定可以知道有人试图窃取 cookie,然后您可以重置每个登录状态。我对吗?
J
Josh Woodcock

我阅读了所有答案,但仍然发现很难提取我应该做的事情。如果一张图片值 1k 字,我希望这有助于其他人基于 Barry Jaspan 的 Improved Persistent Login Cookie Best Practice

https://i.stack.imgur.com/VGbRV.png

如果您有问题、反馈或建议,我将尝试更新图表以反映尝试实现安全持久登录的新手。


t
trante

我的解决方案是这样的。它不是 100% 防弹的,但我认为它会在大多数情况下为您节省开支。

当用户成功登录时,使用此信息创建一个字符串:

$data = (SALT + ":" + hash(User Agent) + ":" + username 
                     + ":" + LoginTimestamp + ":"+ SALT)

加密 $data,将类型设置为 HttpOnly 并设置 cookie。

当用户回到您的网站时,请执行以下步骤:

获取 cookie 数据。删除 cookie 中的危险字符。用 : 字符分解它。检查有效性。如果 cookie 超过 X 天,则将用户重定向到登录页面。如果 cookie 不旧;从数据库中获取最新的密码更改时间。如果在用户上次登录后更改密码,则将用户重定向到登录页面。如果最近没有更改通行证;获取用户当前的浏览器代理。检查是否(currentUserAgentHash == cookieUserAgentHash)。如果代理相同,请转到下一步,否则重定向到登录页面。如果所有步骤都成功通过,则授权用户名。

如果用户退出,请删除此 cookie。如果用户重新登录,则创建新的 cookie。


s
staticsan

实现“让我登录”功能意味着您需要准确定义这对用户意味着什么。在最简单的情况下,我会用它来表示会话的超时时间更长:2 天(比如说)而不是 2 小时。为此,您将需要自己的会话存储,可能在数据库中,因此您可以为会话数据设置自定义到期时间。然后,您需要确保设置的 cookie 会保留几天(或更长时间),而不是在关闭浏览器时过期。

我可以听到你问“为什么是 2 天?为什么不是 2 周?”。这是因为在 PHP 中使用会话会自动将过期时间推回。这是因为 PHP 中会话的到期实际上是空闲超时。

现在,话虽如此,我可能会实现一个更硬的超时值,我将其存储在会话本身中,并在 2 周左右时结束,并添加代码以查看它并强制使会话无效。或者至少将它们注销。这意味着将要求用户定期登录。雅虎!做这个。


设置更长的会话可能很糟糕,因为它会浪费服务器资源并且会对性能产生不利影响
K
K-Gun

我认为你可以这样做:

$cookieString = password_hash($username, PASSWORD_DEFAULT);

$cookiestring 存储在数据库中并将其设置为 cookie。还将此人的用户名设置为 cookie。哈希的全部意义在于它不能被逆向工程。

当用户出现时,从一个 cookie 获取用户名,而不是从另一个 cookie 获取 $cookieString。如果 $cookieString 与数据库中存储的匹配,则用户已通过身份验证。由于 password_hash 每次都使用不同的盐,因此与明文是什么无关。