我的Web应用程序使用会话在用户登录后存储有关用户的信息,并在应用程序中从一个页面移动到另一个页面时维护该信息.在这个特定的应用程序,我存储user_id
,first_name
和last_name
人的.
我想在登录时提供"Keep Me Logged In"选项,这将在用户的计算机上放置一个cookie两周,这将在他们返回应用程序时以相同的细节重新启动他们的会话.
这样做的最佳方法是什么?我不想将它们存储user_id
在cookie中,因为看起来这样可以让一个用户轻松尝试伪造另一个用户的身份.
好吧,让我直言不讳地说:如果你为了这个目的将用户数据或从用户数据派生的任何内容放入cookie中,那么你做错了.
那里.我说了.现在我们可以转到实际的答案.
你问,散列用户数据有什么问题?嗯,它归结为曝光表面和通过默默无闻的安全性.
想象一下,你是一个攻击者.您会在会话中看到为记住我设置的加密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); } } }
注意:请勿使用令牌或用户和令牌的组合来查找数据库中的记录.始终确保根据用户获取记录,并使用计时安全比较功能来比较之后获取的令牌.更多关于定时攻击.
现在,非常重要的SECRET_KEY
是加密秘密(由类似/dev/urandom
和/或从高熵输入得到的东西生成).此外,GenerateRandomToken()
需要一个强大的随机源(mt_rand()
是远远不够强,使用图书馆,如RandomLib或random_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; }
安全注意事项:将cookie从确定性数据的MD5哈希中删除是一个坏主意; 最好使用从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(或几乎任何算法)
介绍
您的标题"让我登录" - 最好的方法让我很难知道从哪里开始,因为如果您正在寻找最佳方法,那么您将不得不考虑以下事项:
鉴定
安全
饼干
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()); }
使用的类
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中存储用户名,令牌和引用
在数据库中存储用户名,令牌,引用,例如.内存缓存
通过get和iframe将refrence id发送到fakeaddsite.com
fakeaddsite.com使用该引用从数据库中获取用户和令牌
fakeaddsite.com存储签名
当用户从fakeaddsite.com返回使用iframe获取签名信息时
合并数据并进行验证
.....你知道剩下的
有些人可能想知道你如何使用2种不同的饼干?那可能,想象example.com = localhost
和fakeaddsite.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
有两篇非常有趣的文章,我在寻找"记住我"问题的完美解决方案时发现:
持久登录Cookie最佳实践
改进的持久登录Cookie最佳实践
我在这里询问了这个问题的一个角度,答案将引导您获得所需的所有基于令牌的超时cookie链接.
基本上,您不会将userId存储在cookie中.您存储一次性令牌(大字符串),用户用它来获取旧的登录会话.然后为了使其真正安全,您需要密码来进行繁重的操作(比如更改密码本身).
我建议Stefan提到的方法(即遵循改进的持久登录Cookie最佳实践中的指导原则),并建议您确保您的cookie是HttpOnly cookie,因此它们无法访问,可能是恶意的JavaScript.