我的web应用程序使用会话存储关于用户的信息,一旦他们登录,并维护这些信息,因为他们在应用程序内从页面到页面。在这个特定的应用程序中,我存储的人的user_id, first_name和last_name。

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

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


当前回答

简介

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

识别 安全

饼干

在常见的浏览器cookie盗窃漏洞和跨站点脚本攻击之间,我们必须接受cookie是不安全的。为了帮助提高安全性,您必须注意php setcookies具有额外的功能,例如

Bool setcookie (string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, Bool $secure = false [, Bool $httponly = false]]]]]])

secure(使用HTTPS连接) httponly(通过XSS攻击减少身份盗窃)

定义

令牌(长度为n的不可预知的随机字符串,例如。/dev/urandom) 参考(长度为n的不可预知的随机字符串,例如。/dev/urandom) 签名(使用HMAC方法生成键控哈希值)

简单的方法

一个简单的解决方案是:

用户使用“记住我”登录 带令牌和签名的登录Cookie 什么时候返回,检查签名 如果签名是ok ..然后在数据库中查找username & token 如果无效..返回登录页面 如果有效,自动登录

上面的案例研究总结了本页上所有的例子,但它们的缺点是

没有办法知道饼干是不是被偷了 攻击者可能是访问敏感操作,如更改密码或数据,如个人和烘焙信息等。 受损害的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中进行测试

优势

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

缺点

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

快速修复

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

多重Cookie方法

当攻击者要窃取cookie时,唯一的焦点是一个特定的网站或域名。example.com

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

用户使用“记住我”登录example.com 存储用户名,令牌,在cookie引用 存储用户名,令牌,在数据库引用等。Memcache 通过get和iframe发送引用id到fakeaddsite.com fakeaddsite.com使用引用从数据库获取用户和令牌 Fakeaddsite.com存储签名 当用户从fakeaddsite.com返回带有iframe的签名信息时 结合数据并进行验证 …你知道剩下的

有些人可能会想,你怎么能使用两个不同的cookie呢?这是可能的,想象example.com = localhost和fakeaddsite.com = 192.168.1.120。如果你检查饼干,它看起来是这样的

从上图来看

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

192.168.1.120

只接受定义的HTTP_REFERER 只接受来自指定REMOTE_ADDR的连接 没有JavaScript,没有内容,但除了签名信息和从cookie中添加或检索它之外没有任何内容

优势

99%的情况下你都能骗过攻击者 您可以在攻击者第一次尝试时轻松锁定该帐户 与其他方法一样,在下次登录之前就可以阻止攻击

缺点

多个请求到服务器,只是为了一个登录

改进

使用iframe使用ajax完成

其他回答

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

通常我会这样做:

User logs in with 'keep me logged in' Create session Create a cookie called SOMETHING containing: md5(salt+username+ip+salt) and a cookie called somethingElse containing id Store cookie in database User does stuff and leaves ---- User returns, check for somethingElse cookie, if it exists, get the old hash from the database for that user, check of the contents of cookie SOMETHING match with the hash from the database, which should also match with a newly calculated hash (for the ip) thus: cookieHash==databaseHash==md5(salt+username+ip+salt), if they do, goto 2, if they don't goto 1

当然,你可以使用不同的cookie名称等,你也可以改变cookie的内容,只是要确保它不容易创建。例如,你也可以在创建用户时创建user_salt,并将其放在cookie中。

你也可以用sha1代替md5(或者几乎任何算法)

Implementing a "Keep Me Logged In" feature means you need to define exactly what that will mean to the user. In the simplest case, I would use that to mean the session has a much longer timeout: 2 days (say) instead of 2 hours. To do that, you will need your own session storage, probably in a database, so you can set custom expiry times for the session data. Then you need to make sure you set a cookie that will stick around for a few days (or longer), rather than expire when they close the browser.

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

现在,我可能会实现一个更难的超时值,我将其存储在会话本身中,并在2周左右退出,并添加代码来查看并强制使会话无效。或者至少让他们退出。这意味着将要求用户定期登录。雅虎这是否。

我推荐Stefan提到的方法(即遵循改进的持久登录Cookie最佳实践中的指导方针),也建议你确保你的Cookie是HttpOnly Cookie,这样它们就不会被潜在的恶意JavaScript访问。

我认为你可以这样做:

$cookieString = password_hash($username, PASSWORD_DEFAULT);

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

当用户出现时,从一个cookie中获取用户名,然后从另一个cookie中获取$cookieString。如果$cookieString与存储在DB中的匹配,则验证用户的身份。由于password_hash每次使用不同的盐,所以它与明文是什么无关。

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

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