"让我登录" - 最好的方法

Mat*_*hew 245 php security session remember-me

我的Web应用程序使用会话在用户登录后存储有关用户的信息,并在应用程序中从一个页面移动到另一个页面时维护该信息.在这个特定的应用程序,我存储user_id,first_namelast_name人的.

我想在登录时提供"Keep Me Logged In"选项,这将在用户的计算机上放置一个cookie两周,这将在他们返回应用程序时以相同的细节重新启动他们的会话.

这样做的最佳方法是什么?我不想将它们存储user_id在cookie中,因为看起来这样可以让一个用户轻松尝试伪造另一个用户的身份.

irc*_*ell 721

好吧,让我直言不讳地说:如果你为了这个目的将用户数据或从用户数据派生的任何内容放入cookie中,那么你做错了.

那里.我说了.现在我们可以转到实际的答案.

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

想象一下,你是一个攻击者.您会在会话中看到为记住我设置的加密cookie.这是32个字符宽.啧啧.那可能是MD5 ......

让我们想象一下,他们知道你使用的算法.例如:

md5(salt+username+ip+salt)
Run Code Online (Sandbox Code Playgroud)

现在,攻击者需要做的就是蛮力"盐"(这不是真正的盐,但稍后会更多),现在他可以用他的IP地址的任何用户名生成他想要的所有假令牌!但强迫盐很难,对吧?绝对.但现代GPU非常擅长.除非你使用足够的随机性(使它足够大),否则它会快速下降,并且随之而来的是城堡的钥匙.

简而言之,唯一能保护你的是盐,它并不像你想象的那样真正保护你.

可是等等!

所有这一切都预示着攻击者知道算法!如果这是秘密和混乱,那么你是安全的,对吧?错了.这种思路有一个名称:安全通过晦涩,永远不应该依赖.

更好的方式

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

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

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

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

possibilities = 2^128
possibilities = 3.4 * 10^38
Run Code Online (Sandbox Code Playgroud)

现在,为了表明这个数字是多么荒谬,让我们想象一下互联网上的每台服务器(比如今天的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
Run Code Online (Sandbox Code Playgroud)

所以每秒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
Run Code Online (Sandbox Code Playgroud)

所以6.8性别秒......

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

215,626,585,489,599 years
Run Code Online (Sandbox Code Playgroud)

甚至更好:

47917 times the age of the universe
Run Code Online (Sandbox Code Playgroud)

是的,这是宇宙年龄的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);
}
Run Code Online (Sandbox Code Playgroud)

然后,验证:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:请勿使用令牌或用户和令牌的组合来查找数据库中的记录.始终确保根据用户获取记录,并使用计时安全比较功能来比较之后获取的令牌.更多关于定时攻击.

现在,非常重要的SECRET_KEY是加密秘密(由类似/dev/urandom和/或从高熵输入得到的东西生成).此外,GenerateRandomToken()需要一个强大的随机源(mt_rand()是远远不够强,使用图书馆,如RandomLibrandom_compat,或mcrypt_create_iv()DEV_URANDOM)...

hash_equals()是为了防止定时攻击.如果您使用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;
}
Run Code Online (Sandbox Code Playgroud)

  • 这很奇怪,因为你的代码与你的答案相矛盾.你说"如果你把用户数据放入一个cookie [...]你做错了什么",但这正是你的代码正在做的事情!是不是更好的从cookie中删除用户名,仅计算令牌上的哈希值(并且可能添加ip地址以防止cookie被盗)然后在rememberMe()中执行fetchUsernameByToken而不是fetchTokenByUserName? (32认同)
  • 自PHP 5.6起,[hash_equals](http://php.net/manual/en/function.hash-equals.php)可用于在进行字符串比较时防止计时攻击. (9认同)
  • 但这种方法是不是意味着任何人都可以使用此用户名和cookie并以任何其他设备的身份登录此用户? (7认同)
  • lol :-),请注意47917年是猜测的最长时间,随机令牌也可以在1小时内猜到. (6认同)
  • @Levit它可以阻止某人获取有效令牌,并更改附加到其上的用户ID. (5认同)
  • 很多东西在这个答案中,但是正如@levit注意到的那样,cookie中的散列是不必要的,因为用户和令牌都存储在数据库中.仅更改cookie中的用户ID将无法帮助攻击者.数据库中的令牌[需要进行哈希处理](http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice/)(它不在此处)以防止在数据库被盗时被滥用.fetchTokenByUserName是一个坏主意,因为你可以从多个电脑,平板电脑,智能手机登录.最好检查令牌和用户名的组合是否在数据库中. (5认同)
  • 我非常喜欢这个答案,这说:hmac如何在这个例子中有所作为.如果其中还有其他未知信息(时间,浏览器信息......),那将是有道理的,但就目前而言,它似乎没有任何区别.如果cookie被盗,那么有或没有hmac它将被接受.如果没有,那么在没有它的情况下,它应该平均需要47917/2个宇宙的生命周期来猜测它(好吧,加上一些安全性,通过`SECRET_KEY`添加的默默无闻;-)).或者我在这里遗漏了什么? (4认同)
  • @Arken不,SECRET_KEY必须在代码中保持安全.如果您的数据库被黑客攻击而不是您的代码,那么您肯定会遇到麻烦,但至少您的秘密是安全的. (3认同)
  • 好的,所以如果秘密留在代码中,它是每次用户加载页面时随机生成的,还是一个永不改变的常量变量? (3认同)
  • @storm_buster可能会在一小时内猜到.(此外,它是宇宙的47917年龄,而不是年.)然而,在宇宙的年龄中猜测它的概率是/ 47917.期望值是47917/2,这似乎是安全的.:-) (2认同)
  • 据我所知,cookie可能只是一个没有加密的用户ID。好的-很难估计的用户ID,但是您存储的正是您需要登录的内容。我是否缺少某些内容? (2认同)
  • 抱歉,为什么在验证功能中间返回false? (2认同)
  • 无论解决方案如何,如果攻击者A直接从受害者V的计算机上窃取cookie,我们就无能为力了,对吗?A可以登录,因为他是V,没有密码.至少直到下次V登录(如果你每次更改cookie).IP检查不是真正的解决方案,因为人们经常更改IP.有人可以说,如果A可以访问V的机器,那时他可以做任何他想做的事情; 事实并非如此,他可以在很短的时间内(足以偷走饼干)进入它,然后用他的机器完成其余的"工作". (2认同)
  • 有人有讨论的全貌或一些 GitHub 存储库代码吗? (2认同)

Pim*_*ger 93

安全注意事项:将cookie从确定性数据的MD5哈希中删除是一个坏主意; 最好使用从CSPRNG派生的随机令牌.有关更安全的方法,请参阅ircmaxell对此问题的回答.

通常我做这样的事情:

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

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

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

  • 为什么在哈希中包含IP?此外,请确保在Cookie中包含时间戳信息,并使用此信息确定Cookie的最大年龄,以便您不会创建有益于永恒的身份令牌. (30认同)
  • @Abhishek Dilliwal:这是一个非常古老的线索,但我遇到它寻找与Mathew相同的答案.我不认为使用session_ID会对Pim的答案起作用,因为你无法检查db hash,cookie hash和当前的session_ID,因为session_ID每次session_start()都会改变; 我以为我会指出这一点. (4认同)
  • 令牌应该是RANDOM,不以任何方式与用户/他的IP /他的useragent /任何东西连接.这是主要的安全漏洞. (4认同)
  • 你为什么用两种盐?MD5(盐+使用者名称+ IP +盐) (4认同)
  • 我很抱歉是沉闷但是第二个饼干的目的是什么?在这种情况下,id是什么?它只是一个简单的"真/假"值来表明用户是否想要使用"保持登录"功能?如果是这样,为什么不直接检查cookie SOMETHING是否存在?如果用户不希望他们的登录持续存在,那么SOMETHING cookie就不会出现在那里吗?最后,您是否再次动态生成哈希并将其作为额外的安全措施来检查cookie和数据库? (3认同)
  • @Pim Jager:我也很好奇为什么你需要包含IP - 你的解决方案主要集中在不希望分配不同IP的桌面Web应用程序上,或者如果应用程序是打算用于各种网络的移动/笔记本电脑设备? (2认同)
  • 在欧洲,大多数消费者互联网提供商会在 24 小时(或更早)后更新 IP,这使得基于 IP 的保持登录身份识别变得不可能。 (2认同)

Bab*_*aba 76

介绍

您的标题"让我登录" - 最好的方法让我很难知道从哪里开始,因为如果您正在寻找最佳方法,那么您将不得不考虑以下事项:

  • 鉴定
  • 安全

饼干

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

bool setcookie(string $ 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中
  • 令牌是随机的,仅对单个身份验证有效
  • 每次访问网站时都会替换令牌
  • 当未登录的用户访问该站点时,将验证签名,令牌和用户名
  • 请记住,登录应具有有限的访问权限,不允许修改密码,个人信息等.

示例代码

// 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());
}
Run Code Online (Sandbox Code Playgroud)

使用的类

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在Firefox和Chrome中测试

在此输入图像描述

优点

  • 更好的安全
  • 攻击者访问受限
  • 当cookie被盗时,它仅对单一访问有效
  • 当下一个原始用户访问该站点时,您可以自动检测并通知用户盗窃

坏处

  • 不支持通过多个浏览器(移动和Web)进行持久连接
  • cookie仍然可以被盗,因为用户只能在下次登录后收到通知.

快速解决

  • 为必须具有持久连接的每个系统引入审批系统
  • 使用多个cookie进行身份验证

多个Cookie方法

当攻击者即将窃取cookie时,只关注特定网站或域名,例如.example.com

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

  • 用户记住我登录到example.com
  • 在cookie中存储用户名,令牌和引用
  • 在数据库中存储用户名,令牌,引用,例如.内存缓存
  • 通过get和iframe将refrence id发送到fakeaddsite.com
  • fakeaddsite.com使用该引用从数据库中获取用户和令牌
  • fakeaddsite.com存储签名
  • 当用户从fakeaddsite.com返回使用iframe获取签名信息时
  • 合并数据并进行验证
  • .....你知道剩下的

有些人可能想知道你如何使用2种不同的饼干?那可能,想象example.com = localhostfakeaddsite.com = 192.168.1.120.如果您检查cookie,它将看起来像这样

在此输入图像描述

从上面的图像

  • 访问的当前站点是localhost
  • 它还包含从192.168.1.120设置的cookie

192.168.1.120

  • 只接受定义 HTTP_REFERER
  • 仅接受来自指定的连接 REMOTE_ADDR
  • 没有JavaScript,没有内容,只包括签名信息,并从cookie添加或检索它

优点

  • 99%的时间你欺骗了攻击者
  • 您可以轻松锁定攻击者首次尝试的帐户
  • 与其他方法一样,即使在下次登录之前也可以防止攻击

坏处

  • 多个请求服务器只需一次登录

起色

  • 完成使用iframe使用 ajax

  • 虽然@ircmaxell很好地描述了这个理论,但我更喜欢这种方法,因为它工作得很好而不需要存储用户ID(这将是一个不需要的公开),并且还包括更多的指纹,而不仅仅是用户ID和哈希来识别用户,例如浏览器.这使得攻击者更难以使用被盗的cookie.这是迄今为止我见过的最好,最安全的方法.+1 (5认同)

Ste*_*rig 23

有两篇非常有趣的文章,我在寻找"记住我"问题的完美解决方案时发现:


Dan*_*ark 6

在这里询问了这个问题的一个角度,答案将引导您获得所需的所有基于令牌的超时cookie链接.

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


小智 6

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

  • 永远不要以明文形式存储密码......永远!
  • 切勿将用户的散列密码存储在数据库中的多个位置。您的服务器后端始终能够从用户表中提取散列密码。存储冗余数据代替额外的 DB 事务并不是更有效,反之亦然。
  • 您的会话ID的应该是唯一的,所以没有两个用户可以永远共享一个ID,一个ID的,因此目的(可能你的驾驶执照身份证号码永远匹配另一个人?号)这会产生一个两件独特的组合,基于2独特的字符串。您的会话表应该使用 ID 作为 PK。要允许多台设备被信任进行自动登录,请使用另一个包含所有已验证设备列表的受信任设备表(请参见下面的示例),并使用用户名进行映射。
  • 将已知数据散列到 cookie 中没有任何意义,cookie 可以被复制。我们正在寻找的是一种合规的用户设备,以提供在攻击者不损害用户机器的情况下无法获得的真实信息(再次参见我的示例)。然而,这意味着禁止其机器的静态信息(即 MAC 地址、设备主机名、如果受浏览器限制的用户代理等)保持一致(或首先欺骗它)的合法用户将无法使用此功能。但如果这是一个问题,请考虑这样一个事实,即您正在向唯一标识自己的用户提供自动登录,所以如果他们通过欺骗他们的 MAC、欺骗他们的用户代理、欺骗/更改他们的主机名、隐藏在代理后面等方式拒绝被认识,那么他们就无法识别,并且永远不应该为自动服务进行身份验证。如果你想要这个,你需要研究与客户端软件捆绑在一起的智能卡访问,为正在使用的设备建立身份。

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

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

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

由于外部身份验证器用于在后台告诉服务器用户是否经过验证,因此攻击者除了唯一 ID 之外无法获得任何其他信息,而这本身是无用的。我会详细说明:

  • 用户 'joe' 第一次访问站点,会话 ID 放置在 cookie 'session' 中。
  • 用户 'joe' 登录,提升权限,获取新的会话 ID 并更新 cookie '会话'。
  • 用户 'joe' 选择使用 google+ 自动登录,并在 cookie 'keepmesignedin' 中获得一个唯一 ID。
  • 用户“joe”让谷歌让他们保持登录状态,允许您的网站在后端使用谷歌自动登录用户。
  • 攻击者系统地尝试使用唯一的“keepmesignedin”ID(这是分发给每个用户的公共知识),并且不会登录到其他任何地方;尝试给 'joe' 的唯一 ID。
  • 服务器收到“joe”的唯一 ID,在数据库中为 google+ 帐户提取匹配项。
  • 服务器将攻击者发送到运行 AJAX 请求到谷歌登录的登录页面。
  • Google 服务器收到请求,使用其 API 来查看 Attacker 当前未登录。
  • Google 发送响应,指出当前没有通过此连接登录的用户。
  • 攻击者的页面收到响应,脚本自动重定向到登录页面,在 url 中编码 POST 值。
  • 登录页面获取 POST 值,将 'keepmesignedin' 的 cookie 发送到一个空值和 1-1-1970 的有效截止日期以阻止自动尝试,从而导致攻击者的浏览器简单地删除 cookie。
  • 攻击者获得正常的首次登录页面。

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

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

==========

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

DB有几个表:

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

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

TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL
Run Code Online (Sandbox Code Playgroud)

注意这个表中没有用户字段,因为用户名在登录时是在会话数据中的,程序不允许空数据。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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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


Wal*_*sby 5

我建议Stefan提到的方法(即遵循改进的持久登录Cookie最佳实践中的指导原则),并建议您确保您的cookie是HttpOnly cookie,因此它们无法访问,可能是恶意的JavaScript.