Php &引用;让我保持登录状态";-最佳方法
我的web应用程序使用会话来存储用户登录后的信息,并在用户在应用程序中从一个页面移动到另一个页面时维护这些信息。在这个特定的应用程序中,我存储的是这个人的Php &引用;让我保持登录状态";-最佳方法,php,security,session,remember-me,Php,Security,Session,Remember Me,我的web应用程序使用会话来存储用户登录后的信息,并在用户在应用程序中从一个页面移动到另一个页面时维护这些信息。在这个特定的应用程序中,我存储的是这个人的用户名、名和姓 我想在登录时提供一个“让我保持登录”选项,该选项将在用户的机器上放置一个cookie两周,当用户返回应用程序时,将使用相同的详细信息重新启动会话 这样做的最佳方法是什么?我不想将他们的用户id存储在cookie中,因为这似乎会让一个用户很容易伪造另一个用户的身份。生成一个散列,可能只有您知道一个秘密,然后将其存储在您的DB中,以
用户名
、名
和姓
我想在登录时提供一个“让我保持登录”选项,该选项将在用户的机器上放置一个cookie两周,当用户返回应用程序时,将使用相同的详细信息重新启动会话
这样做的最佳方法是什么?我不想将他们的
用户id
存储在cookie中,因为这似乎会让一个用户很容易伪造另一个用户的身份。生成一个散列,可能只有您知道一个秘密,然后将其存储在您的DB中,以便它可以与用户关联。应该很管用
安全注意事项:将cookie基于确定性数据的MD5散列是个坏主意;最好使用从CSPRNG派生的随机令牌。查看此问题以获得更安全的方法
通常我会这样做:
您也可以使用sha1而不是md5(或几乎任何算法)我问了这个问题的一个角度,答案将引导您找到所需的所有基于令牌的超时cookie链接 基本上,您不会将用户ID存储在cookie中。您存储了一个一次性令牌(大字符串),用户使用它来选择他们的旧登录会话。然后,为了使其真正安全,您需要为繁重的操作(如更改密码本身)请求密码。实现“让我登录”功能意味着您需要准确定义这对用户意味着什么。在最简单的情况下,我会用它来表示会话有更长的超时时间:比如说2天而不是2小时。要做到这一点,您需要自己的会话存储,可能在数据库中,这样您就可以为会话数据设置自定义的过期时间。然后,您需要确保设置的cookie将保留几天(或更长),而不是在浏览器关闭时过期 我听到你在问“为什么两天?为什么不是两周?”。这是因为在PHP中使用会话会自动将到期日期向后推。这是因为PHP中的会话到期实际上是一个空闲超时
话虽如此,我可能会实现一个更硬的超时值,我将它存储在会话本身中,并在大约2周后输出,然后添加代码以查看该值并强制使会话无效。或者至少把他们注销。这意味着用户将被要求定期登录。雅虎!这样做。我建议采用Stefan提到的方法(即遵循中的指导原则),并建议您确保您的cookie是可访问的,因此JavaScript可能是恶意的。好吧,让我直截了当地说:如果您正在放置用户数据,或者从用户数据派生到cookie中的任何东西,都是错误的 在那里。我说了。现在我们可以进入实际答案 你会问,散列用户数据有什么问题?好吧,归根结底是暴露表面和通过模糊的安全性 想象一下,你是一个攻击者。您会在会话中看到“记住我”的加密cookie集。它有32个字符宽。哇那可能是MD5 让我们想象一下,他们知道你使用的算法。例如:
md5(salt+username+ip+salt)
现在,攻击者所需要做的就是强行使用“salt”(这不是真正的salt,但稍后会详细介绍),现在他可以使用任何用户名为其IP地址生成所有他想要的假令牌!但是用蛮力逼盐很难,对吧?绝对地但现代GPU非常擅长于此。除非你在里面使用足够的随机性(使它足够大),否则它会很快掉下来,并伴随着你城堡的钥匙
简言之,唯一能保护你的是盐,它并不像你想象的那样能真正保护你
等等强>
所有这些都表明攻击者知道算法!如果这是秘密和混乱,那么你是安全的,对吗错误。这一思路有一个名字:通过默默无闻实现安全,永远不应该依赖它
更好的方法
更好的方法是永远不要让用户的信息离开服务器,id除外
当用户登录时,生成一个大的(128到256位)随机令牌。将其添加到将令牌映射到userid的数据库表中,然后将其发送到cookie中的客户端
如果攻击者猜测到另一个用户的随机令牌怎么办
好吧,让我们在这里做一些数学。我们正在生成一个128位随机令牌。这意味着:
possibilities = 2^128
possibilities = 3.4 * 10^38
现在,为了说明这个数字有多大,让我们想象一下互联网上的每台服务器(比如今天的50000000台)都试图以1,00的速度强行计算这个数字
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
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
215,626,585,489,599 years
47917 times the age of the universe
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);
}
}
}
/**
* 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;
}
// 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);
}
}
$data = (SALT + ":" + hash(User Agent) + ":" + username
+ ":" + LoginTimestamp + ":"+ SALT)
TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...
TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL
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
$cookieString = password_hash($username, PASSWORD_DEFAULT);