我的网络应用程序使用会话在用户登录后存储有关用户的信息,并在应用程序中从一个页面移动到另一个页面时维护该信息。在此特定应用程序中,我将存储此人的user_id
,first_name
和last_name
。
我想在登录时提供“保持登录状态”选项,这将在用户的计算机上放置一个cookie两周,这将在他们返回应用程序时以相同的详细信息重新启动他们的会话。 / p>
这样做的最佳方法是什么?我不想将他们的user_id
存储在cookie中,因为这似乎可以让一个用户轻松尝试伪造另一个用户的身份。
答案 0 :(得分:685)
好的,让我直言不讳地说:如果您为此目的将用户数据或从用户数据派生的任何内容放入cookie中,那么您做错了。
有。我说了。现在我们可以转到实际答案。
您要问,散列用户数据有什么问题?嗯,它归结为曝光表面和通过默默无闻的安全性。
想象一下你是攻击者。您会在会话中看到为记住我设置的加密cookie。这是32个字符宽。啧啧。那可能是MD5 ......
让我们想象一下,他们知道您使用的算法。例如:
md5(salt+username+ip+salt)
现在,攻击者需要做的就是暴力破解“盐”(这不是真正的盐,但稍后会更多),现在他可以用他的任何用户名生成他想要的所有假代币。 IP地址!但强迫盐很难,对吧?绝对。但现代GPU非常擅长。除非你在它中使用足够的随机性(使它足够大),它会迅速下降,并随之成为你城堡的钥匙。
简而言之,唯一保护你的是盐,它并不像你想象的那样真正保护你。
但等等!
所有这一切都预示着攻击者知道算法!如果这是秘密和混乱,那么你是安全的,对吧?的 WRONG 即可。这种想法有一个名称:安全通过晦涩,应该从不依赖。
更好的方式
更好的方法是永远不要让用户的信息离开服务器,除了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()
不够强大。请使用库,例如RandomLib或random_compat或mcrypt_create_iv()
与DEV_URANDOM
)...
hash_equals()
是为了阻止timing attacks。
如果您使用PHP 5.6以下的PHP版本,则不支持函数hash_equals()
。在这种情况下,您可以使用timingSafeCompare函数替换hash_equals()
:
/**
* 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;
}
答案 1 :(得分:94)
安全通知:根据确定性数据的MD5哈希来设置cookie是一个坏主意;最好使用从CSPRNG派生的随机令牌。有关更安全的方法,请参阅ircmaxell's answer此问题。
通常我会做这样的事情:
当然,您可以使用不同的cookie名称等。您也可以稍微更改cookie的内容,只需确保不要轻易创建。例如,您还可以在创建用户时创建user_salt,并将其放入cookie中。
你也可以使用sha1而不是md5(或几乎任何算法)
答案 2 :(得分:74)
<强>简介强>
您的头衔“让我登录” - 最好的方法让我很难知道从哪里开始,因为如果您正在寻找最佳方法,那么您将不得不考虑以下因素: / p>
<强>缓存强>
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]]]]]])
定义
简单方法
一个简单的解决方案是:
上述案例研究总结了本页面给出的所有示例,但它们的缺点是
更好的解决方案
更好的解决方案是
示例代码
// 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中测试&amp; Chrome
优势
缺点
快速修复
多Cookie方法
当攻击者即将窃取cookie时,只关注特定网站或域名,例如。的 example.com 强>
但实际上,您可以对来自2个不同域( example.com &amp; fakeaddsite.com )的用户进行身份验证,并使其看起来像&#34;广告Cookie&# 34;
有些人可能想知道你如何使用2种不同的饼干?好吧,想象一下example.com = localhost
和fakeaddsite.com = 192.168.1.120
。如果您检查cookie,它将看起来像这样
从上图
192.168.1.120
HTTP_REFERER
REMOTE_ADDR
优势
缺点
改进
ajax
答案 3 :(得分:24)
我在寻找“记住我”问题的完美解决方案时发现了两篇非常有趣的文章:
答案 4 :(得分:6)
我问过这个问题的一个角度here,答案会引导您找到所需的所有基于令牌的超时Cookie链接。
基本上,您不会将userId存储在cookie中。您存储一次性令牌(大字符串),用户用它来获取旧的登录会话。然后为了使其真正安全,您需要密码来进行繁重的操作(比如更改密码本身)。
答案 5 :(得分:5)
我会推荐Stefan提到的方法(即遵循Improved Persistent Login Cookie Best Practice中的指导原则),并建议您确保您的Cookie为HttpOnly cookies,以便它们无法访问,可能是恶意的JavaScript。
答案 6 :(得分:4)
生成哈希值,可能只有您知道的秘密,然后将其存储在您的数据库中,以便它可以与用户关联。应该工作得很好。
答案 7 :(得分:4)
旧线程,但仍然是一个有效的关注点。我注意到一些关于安全性的好的回答,并且避免通过默默无闻来使用“安全性”,但实际的技术方法在我眼中是不够的。在我提出方法之前我必须说的话:
尽管如此,有两种方法可以在您的系统上进行自动登录。
首先,廉价,简单的方式将其全部放在别人身上。如果您使用谷歌+帐户登录您的网站支持,您可能有一个简化的谷歌+按钮,如果他们已经登录谷歌,将记录用户(我这样做是为了回答这个问题,因为我总是签到谷歌)。如果您希望用户自动登录,如果他们已经使用受信任和支持的身份验证器登录,并选中了复选框,请让您的客户端脚本执行相应的&#39;登录后面的代码&# 39;加载前按钮,只需确保服务器在自动登录表中存储唯一ID,该表具有用户名,会话ID和用户身份验证器。由于这些登录方法使用AJAX,因此您无论如何都在等待响应,并且该响应是经过验证的响应或拒绝。如果您获得经过验证的响应,请正常使用它,然后继续正常加载登录用户。否则,登录失败,但不告诉用户,只要继续登录,他们就会注意到。这是为了防止攻击者窃取cookie(或伪造他们以试图升级特权),以防止用户自动登录该网站。
这很便宜,也可能被一些人视为肮脏,因为它试图验证您可能已经在Google和Facebook这样的地方签名,甚至没有告诉您。但是,它不应该用于未要求自动登录您网站的用户,并且此特定方法仅适用于外部身份验证,例如Google+或FB。
由于外部身份验证器用于在后台告知服务器用户是否经过验证,因此攻击者无法获得除唯一ID之外的任何内容,而该ID本身无用。我将详细说明:
无论如何,即使攻击者使用不存在的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地址本质上应该是独一无二的,因此每个条目都具有唯一值是有道理的。另一方面,主机名可以合法地在不同的网络上复制。有多少人使用&#34; Home-PC&#34;作为他们的计算机名称之一?用户名是由服务器后端从会话数据中获取的,因此操作它是不可能的。至于令牌,应该使用相同的方法为页面生成会话令牌,以便为用户自动登录生成cookie中的令牌。最后,添加日期时间代码以供用户在需要重新验证其凭据时使用。要么在用户登录时更新此日期时间,要么在几天内将其保留,要么强制它过期,无论上次登录只保留一个月左右,无论您的设计要求如何。
这可以防止有人为他们知道自动登录的用户系统地欺骗MAC和主机名。 从不 让用户使用密码保存Cookie,明文或者其他。让令牌在每个页面导航上重新生成,就像会话令牌一样。这大大降低了攻击者获取有效令牌cookie并使用它登录的可能性。有些人会试图说攻击者可以从受害者那里窃取cookie并进行会话重播攻击以登录。如果攻击者可以窃取cookie(这是可能的),他们肯定会破坏整个设备,这意味着他们可能只是使用设备登录,这完全违背了窃取cookie的目的。只要您的站点通过HTTPS(在处理密码,CC号码或其他登录系统时应该运行),您就可以在浏览器中为用户提供所有保护。
要记住一件事:如果您使用自动登录,会话数据不应过期。您可以使错误地继续会话的能力到期,但如果会话数据是预期在会话之间继续的持久数据,则验证到系统应该恢复会话数据。如果你想要持久和非持久会话数据,使用另一个表作为持久会话数据,用户名作为PK,并让服务器像正常会话数据一样检索它,只需使用另一个变量。
以这种方式实现登录后,服务器仍应验证会话。您可以在此处编制对被盗或受损系统的期望;登录到会话数据的模式和其他预期结果通常可以得出结论,系统被劫持或cookie被伪造以获得访问权。这是您的ISS技术可以制定规则,可以触发帐户锁定或自动从自动登录系统中删除用户,使攻击者远离攻击者,以便用户确定攻击者如何成功以及如何将其删除。
作为结束语,请确保任何恢复尝试,密码更改或登录失败超过阈值都会导致自动登录被禁用,直到用户正确验证并确认已发生此情况。
如果有人希望在我的回答中提供代码,我很抱歉,这不会发生在这里。我会说我使用PHP,jQuery和AJAX来运行我的网站,我永远不会使用Windows作为服务器......
答案 8 :(得分:2)
我的解决方案是这样的。它不是100%防弹,但我认为它可以为你节省大部分时间。
当用户登录成功创建包含此信息的字符串时:
$data = (SALT + ":" + hash(User Agent) + ":" + username
+ ":" + LoginTimestamp + ":"+ SALT)
加密$data
,将类型设置为HttpOnly并设置Cookie。
当用户返回您的网站时,请执行以下步骤:
:
字符爆炸它。 如果用户退出,请删除此Cookie。如果用户重新登录,则创建新的cookie。
答案 9 :(得分:2)
我不理解将加密的东西存储在cookie中的概念,当它是你需要进行黑客攻击的加密版本时。如果我遗漏了什么,请发表评论。
我正在考虑采用这种方法来“记住我”。如果您发现任何问题,请发表评论。
创建一个表来存储“记住我”数据 - 与用户表分开,以便我可以从多个设备登录。
成功登录后(记住我勾选):
a)生成一个唯一的随机字符串,用作此机器上的UserID:bigUserID
b)生成一个唯一的随机字符串:bigKey
c)存储cookie:bigUserID:bigKey
d)在“记住我”表格中,添加一条记录:UserID,IP Address,bigUserID,bigKey
如果尝试访问需要登录的内容:
a)检查cookie并搜索bigUserID&amp;具有匹配IP地址的bigKey
b)如果找到它,请记录此人,但在用户表“软登录”中设置一个标志,以便对于任何危险操作,您可以提示完整登录。
注销时,将该用户的所有“记住我”记录标记为已过期。
我能看到的唯一漏洞是
答案 10 :(得分:2)
我阅读了所有的答案,但仍然发现很难提取我应该做的事情。如果图片价值1k字,我希望这可以帮助其他人实现基于Barry Jaspan Improved Persistent Login Cookie Best Practice
的安全持久存储如果您有任何问题,反馈或建议,我会尝试更新图表,以反映尝试实施安全持久登录的新手。
答案 11 :(得分:0)
实施“保持登录状态”功能意味着您需要准确定义对用户意味着什么。在最简单的情况下,我会用它来表示会话有更长的超时:2天(比方说)而不是2小时。为此,您可能需要自己的会话存储(可能在数据库中),因此您可以为会话数据设置自定义到期时间。然后,您需要确保设置一个cookie,它会在几天(或更长时间)内停留,而不是在关闭浏览器时过期。
我可以听到你问“为什么2天?为什么不是2周?”。这是因为在PHP中使用会话将自动推迟到期。这是因为PHP中的会话到期实际上是空闲超时。
现在,说到这一点,我可能会实现一个更难的超时值,我存储在会话本身,并在2周左右,并添加代码来查看并强制使会话无效。或者至少将它们记录下来。这将意味着将要求用户定期登录。雅虎这样做。
答案 12 :(得分:0)
我认为您可以这样做:
$cookieString = password_hash($username, PASSWORD_DEFAULT);
将$cookiestring
存储在数据库中,并将其设置为cookie。还要将该人的用户名设置为cookie。哈希的全部要点是它不能被反向工程。
当用户出现时,从一个cookie获取用户名,而不是从另一个$cookieString
获取用户名。如果$cookieString
与数据库中存储的匹配,则对用户进行身份验证。由于password_hash每次都使用不同的盐,因此与明文内容无关。