Answers:
好的,让我直言不讳:如果您为此目的将用户数据或从用户数据派生的任何内容放入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
现在,为了说明这个数字有多么荒谬,让我们想象一下互联网上的每台服务器(今天的数字是5000万)试图以每秒10亿的速度强行暴力破解该数字。实际上,您的服务器会在这样的负载下融化,但让我们来解决一下。
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对这个问题的回答。
通常我会这样:
当然,您可以使用其他Cookie名称等。也可以稍微更改Cookie的内容,只需确保不要轻易创建它即可。例如,您还可以在创建用户时创建一个user_salt,并将其放入cookie中。
您也可以使用sha1代替md5(或几乎所有算法)
介绍
您的标题为“保持登录状态”-最佳方法使我很难知道从哪里开始,因为如果您正在寻找最佳方法,则必须考虑以下事项:
饼干
Cookies是易受攻击的,在常见的浏览器cookie盗窃漏洞和跨站点脚本攻击之间,我们必须接受cookie不安全。为了帮助提高安全性,您必须注意php
setcookies
具有其他功能,例如
bool setcookie(字符串$ name [,字符串$ value [,整数$ expire = 0 [,字符串$ path [,字符串$ domain [,布尔$ secure =假[,布尔$ httponly =假]]]]]]]))
定义
简单方法
一个简单的解决方案是:
以上案例研究总结了此页面上给出的所有示例,但它们的缺点是
更好的解决方案
更好的解决方案是
范例程式码
// 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方法
当攻击者要窃取cookie时,只需将其集中在特定的网站或域上即可。example.com
但实际上您可以对来自2个不同域(example.com和fakeaddsite.com)的用户进行身份验证,并使其看起来像“广告Cookie”
有人可能会怀疑您如何使用2种不同的Cookie?好吧,想象example.com = localhost
和fakeaddsite.com = 192.168.1.120
。如果您检查Cookie,它将看起来像这样
从上图
192.168.1.120
HTTP_REFERER
REMOTE_ADDR
优点
坏处
改善
ajax
旧线程,但仍然是一个值得关注的问题。我注意到有关安全性的一些很好的响应,并避免使用“默默无闻的安全性”,但是在我看来,给出的实际技术方法还不够。我贡献自己的方法之前必须说的话:
综上所述,有两种方法可以在您的系统上进行自动登录。
首先,一种便宜,简单的方法将所有这些都放在别人身上。如果您使网站支持使用google +帐户登录,则可能有一个简化的google +按钮,如果用户已经登录google,则可以登录(我一直在这里回答这个问题,因为我一直登录到Google)。如果您希望用户在已经使用受信任和受支持的身份验证器登录后自动登录,并且选中了该复选框,请在加载之前让客户端脚本在相应的“登录方式”按钮后面执行代码,只需确保服务器在自动登录表中存储一个唯一ID,即可包含用户名,会话ID和用于用户的身份验证器。由于这些登录方法使用AJAX,因此无论如何您都在等待响应,该响应要么是经过验证的响应,要么是拒绝。如果您收到经过验证的响应,请照常使用它,然后照常继续加载登录的用户。否则,登录将失败,但不要告诉用户,只是继续以未登录状态,他们会注意到。这是为了防止窃取Cookie(或伪造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哈希,添加了随机字符串然后进行哈希处理的datetime时间戳生成,也可以根据需要生成,但是输出的熵应保持在可容忍的范围内。避免暴力攻击甚至落地,在尝试添加会话类之前,应检查会话表中生成的所有哈希是否匹配。
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地址本质上应该是唯一的,因此每个条目都具有唯一值是有意义的。另一方面,主机名可以合法地在单独的网络上重复。有多少人将“家用PC”用作他们的计算机名称之一?用户名由服务器后端从会话数据中获取,因此无法进行操作。至于令牌,应使用与生成页面会话令牌相同的方法在Cookie中为用户自动登录生成令牌。最后,为用户何时需要重新验证其凭据添加了日期时间代码。在用户登录时更新此日期时间,以使其在几天之内保持不变,或者无论最后一次登录如何,都强制使其过期,仅将其保留一个月左右,这取决于您的设计要求。
这样可以防止某人为他们认识的自动登录用户系统地欺骗MAC和主机名。让用户使用其密码,明文或其他方式保存cookie。与会话令牌一样,在每个页面导航上重新生成令牌。这极大地降低了攻击者获得有效令牌cookie并将其用于登录的可能性。有人会说攻击者可以从受害者那里窃取cookie并进行会话重播攻击以登录。如果攻击者可以窃取cookie(可能),则他们肯定会破坏整个设备,这意味着他们无论如何都可以使用该设备登录,这完全破坏了窃取cookie的目的。只要您的站点运行在HTTPS上(在处理密码,CC号码或其他登录系统时,它就应该运行),您就可以在浏览器中为用户提供所有保护。
要记住的一件事:如果使用自动登录,则会话数据不应过期。您可以终止继续错误地继续会话的功能,但是,如果系统是持久性数据,并且应该在会话之间继续进行,则验证进入系统后应该恢复会话数据。如果您同时需要持久性和非持久性会话数据,请使用另一个表来存储以用户名作为PK的持久性会话数据,并让服务器像正常会话数据一样检索它,只需使用另一个变量即可。
以这种方式完成登录后,服务器仍应验证会话。在这里,您可以为被盗或受感染的系统编写期望代码。模式和登录会话数据的其他预期结果通常可以得出这样的结论,即系统被劫持或伪造了cookie以获取访问权限。在这里,您的ISS Tech可以制定规则,触发帐户锁定或自动将用户从自动登录系统中删除,从而使攻击者保持足够长的时间,以便用户确定攻击者如何成功以及如何将其切断。
作为结束语,请确保任何恢复尝试,密码更改或登录失败超出阈值都会导致自动登录被禁用,直到用户正确验证并确认已发生这种情况为止。
如果有人期望在我的答案中给出代码,我深表歉意,这不会在这里发生。我会说我使用PHP,jQuery和AJAX来运行我的站点,并且我从不使用Windows作为服务器。
我建议使用Stefan提到的方法(即遵循“ 改进的持久登录Cookie最佳实践”中的指南),并且还建议您确保cookie是HttpOnly cookie,这样它们就不能被潜在的恶意JavaScript访问。
我的解决方案是这样的。它不是100%防弹的,但我认为它将在大多数情况下为您节省。
用户成功登录后,使用以下信息创建一个字符串:
$data = (SALT + ":" + hash(User Agent) + ":" + username
+ ":" + LoginTimestamp + ":"+ SALT)
加密$data
,将类型设置为HttpOnly并设置cookie。
当用户回到您的网站时,请执行以下步骤:
:
角色爆炸。如果用户退出,请删除此cookie。如果用户重新登录,则创建新的cookie。
我不了解将加密内容存储在Cookie中的概念,因为它是您需要进行黑客入侵的加密版本。如果我缺少什么,请发表评论。
我正在考虑采用这种方式来“记住我”。如果您看到任何问题,请发表评论。
创建一个表来存储“记住我”数据-与用户表分开,以便我可以从多个设备登录。
成功登录后(选中“记住我”):
a)生成一个唯一的随机字符串以用作此计算机上的UserID:bigUserID
b)生成唯一的随机字符串:bigKey
c)存储一个cookie:bigUserID:bigKey
d)在“记住我”表中,添加一条记录:UserID,IP地址,bigUserID,bigKey
如果尝试访问需要登录的内容:
a)检查cookie并搜索具有匹配IP地址的bigUserID和bigKey
b)如果找到它,请登录该人,但在用户表“ soft login”中设置一个标志,这样,对于任何危险的操作,您都可以提示您进行完全登录。
注销后,将该用户的所有“记住我”记录标记为已过期。
我唯一看到的漏洞是;
我阅读了所有答案,但仍然很难提取我应该做的事情。如果一张图片价值一千个单词,我希望这可以帮助其他人基于Barry Jaspan的改进的持久登录Cookie最佳实践实现安全的持久存储
如果您有任何疑问,反馈或建议,我将尝试更新该图以反映尝试实施安全持久登录的新手。
实施“保持登录状态”功能意味着您需要准确定义对用户意味着什么。在最简单的情况下,我将用它来表示会话的超时时间更长:(例如)2天而不是2小时。为此,您将需要自己的会话存储(可能在数据库中),因此您可以为会话数据设置自定义到期时间。然后,您需要确保设置的Cookie会保留几天(或更长时间),而不是在它们关闭浏览器时过期。
我听到您问“为什么2天?为什么不2周?”。这是因为在PHP中使用会话会自动将到期日推迟。这是因为PHP中的会话有效期实际上是一个空闲超时。
现在,话虽如此,我可能会实现一个更硬的超时值,该值存储在会话本身中,并在2周左右的时间内超时,并添加代码以查看该值并强制使会话无效。或者至少将其注销。这意味着将要求用户定期登录。雅虎!做这个。
我认为您可以这样做:
$ cookieString = password_hash($ username,PASSWORD_DEFAULT);
将$ cookiestring存储在数据库中,并将其设置为cookie。还要将该人的用户名设置为cookie。哈希的全部要点是它不能被反向工程。
当用户出现时,从一个cookie获取用户名,而不是从另一个cookie获取$ cookieString。如果$ cookieString与数据库中存储的$ cookieString相匹配,则对用户进行身份验证。由于password_hash每次都使用不同的盐,因此与明文内容无关。