PHP网站中保持登录状态的功能是怎么做的?

大多数网站在用户登录时会提供一个“记住我”或者“保持登录状态”的选项,我只知道是用Cookie实现的,但是具体来说的工作流程是什么?因为要实现一个网站…
关注者
262
被浏览
62,258

11 个回答

我来谈谈这个问题吧
首先,基于以上几位朋友提到的,SESSION信息存储在服务端,相对于存储在客户端的COOKIE更为安全,所以正常一般网站在用于“判断用户是否登录”时,确实是使用SESSION,例如可以在SESSION里存储如下一个数组

//验证用户名和密码成功后
$_SESSION['userinfo'] = [
  'uid' => 123,
  'username' => 'testuser'
];

而后在需要验证登录的地方加入类似如下判断

if(empty($_SESSION['userinfo']) || empty($_SESSION['userinfo']['uid'])){
  //未登录,引导登录
}

以上是使用SESSION做用户登录的基本存储和验证逻辑,当然实际开发过程中会将这部分的代码封装

我们都知道,SESSION一般是通过在COOKIE里记录一个KEY为PHPSESSID的COOKIE来保持上下文的,而这个PHPSESSID的COOKIE的有效期是设置为“会话”,意味着关闭浏览器后该COOKIE被销毁,相应的后端SESSION也就销毁,如图


另外,由于PHP的Session本身就有GC的机制,一般默认1440秒内页面没有刷新动作(准确的说是没有新的请求来刷新该PHPSESSID的生命周期),该SESSION也就被自动回收,伴随着用户的登录就失效了。

而题主关注的是如何实现这个“记住我”的功能,首先,虽然COOKIE保存在客户端,不安全,易被伪造,这是客观存在的事实,但要实现这个“记住我”,还确实就得用到COOKIE,我们能做的是尽量去提高伪造的门槛。
这边仅提供一个参考的设计方案,
1、将用户信息,比如一个['uid'=>123, 'username'=>'testuser']的数组,序列化后成为字符串,使用可逆加密算法加密该字符串,写到一个Key为userinfo的COOKIE里
2、由于可逆加密算法容易被解密,一旦加密的规则被别人猜测到以后,就可以轻易篡改这个COOKIE的内容,然后自行根据加密规则加密后伪造,所以,我们另外加入一个infodig的COOKIE,是将以上的userinfo的COOKIE内容,加入salt后使用不可逆加密算法生成散列,至于salt咱们可以自己定,总之要对外保密,不可逆算法例如md5,甚至多次加盐多次md5
3、以上两个COOKIE,为增强安全性,防止用户被XSS攻击后拿到,可以设置http-only属性

服务端判断存在以上两个COOKIE后
1、验证infodig与userinfo是否匹配(将userinfo的内容使用生成infodig的方法计算后,与COOKIE传上来的infodig匹配是否一致)
2、infodig验证通过后,使用解密算法解密userinfo串,得到用户信息,如果用户信息里的uid存在用户表中,则写SESSION,通过SESSION保持本次会话


总而言之,使用COOKIE记录用户信息是可行的(当然不建议把用户敏感的东西存在COOKIE,例如邮箱、手机、甚至密码,只记录对登录有用的部分,例如uid、username等标识,以及nickname可能会在某些地方提升用户体验),可以确定的是,这个COOKIE对用户可见,我们要做的就是两点:1、尽量让用户看不懂,而只有我们服务端自己认识(可逆加密算法); 2、即使用户看懂了,他也不能够轻易的伪造(不可逆的散列算法)

先说说结论:

1.cookie和session都可以用来保持登录状态。

2.如果使用cookie,用于保存用户状态的cookie需要加密,并且可被识别,但不必须可以解密。这个意思就是说,加密后的存储了用户登录信息的cookie数据在服务端可被还原,即使不能还原也要可以用于识别和比对(即唯一),php的加密函数可以去网上找,一搜一大把。

3.如果使用session,在不考虑安全的情况下,可以简单的使用$_SESSION这个全局数组达到目的,但是如果需要更高的安全性,需要额外的参数进行安全性验证,可以是加密字符串也可以是某串数据的MD5指纹。

4.使用session可以保证登录数据的一致性,如果登录数据中有在登录时间内可能会发生变动的项目,但是依然需要用到额外的参数去提取原来的session数据。

5.cookie比起session会有个滞后性,这一点需要注意,有时候有益,有时候也不方便。

6.在完全理解session的工作机制后,可以尝试抛弃php本身的session机制,建立自定义session机制。

终极结论:

为了保证安全性,既然都需要加密cookie,那么,为什么不把用户登录数据都存储在cookie里呢?session还会浪费服务器端存储。

终极进阶结论:

当你学会使用内存数据库的时候,比如memcached或者redis之类的,cookie和它们结合才是绝佳的解决方案。

下面扯点别的:

说起状态维持这件事情,一直是http的痛处,因为http是无状态协议,要让无状态协议支持状态维持,于是产生了cookie,早期浏览器对cookie戒心甚大,于是勉为其难用一下GET参数也是无妨,现代浏览器都是默认开启cookie的,甚至大部分都已经支持了浏览器端存储(storage)以及少部分支持了浏览器端数据库,后两者仅是存储,在请求时并不会向服务器反馈数据。而session是建立在cookie或者其他从浏览器传回的数据之上的,脱离了cookie或者其他从浏览器传回的参数(比如GET参数)就是无源之水、无根之木。

有人说,甚至有不少人说,session比cookie安全,我觉得这是不恰当的看法。cookie和session具有同等的安全性,并不会因为cookie存储于客户端而session存储于服务端而使cookie的安全性低于session,可以这么说,cookie和session的安全性都取决于cookie或者前文提到的传回服务器的参数的安全性。

而且session也不仅仅是局限于php本身提供的session库,任何存储于服务器端的数据都有可能扮演session的角色,本质上session是一种特殊的数据库,只不过这个数据库只允许已被识别的唯一合法客户端去访问。

虽然我在上面一段文字中强调了唯一,但是识别过程并不可靠,也就是有可能不唯一,这就是所谓的不安全性的来源。事实上,自从网络诞生以来,其上一直充斥的欺骗与暴力。这世上没有任何事情是唯一的,就像没有任何事物是完全相同的,虽然哲学一直教导我们世界上没有两件完全一样的事物,也没有两件完全独立的事物,但是这种绝对的观念并不适合相对的生活。生活中,存在唯一这个概念,然而令人遗憾的是并没有唯一的事实。即使有个枕边人温言细语的告诉你,你是他的唯一,如果你真信了,那只能是你太天真。唯一这个事情是有分辨粒度的,在可被识别的范围内,唯一才是有意义的,但是这种有意义却着实令人心碎,可识别意味着判断一个事物是否唯一的规则是有限的,有限就意味着可仿冒,时至今日,人类创造了各种用于识别唯一性的技术,但是关于识别技术,却依然在发展,而且永无止境。

最后的最后,附赠几段cookie操作函数:

特性1:去除了cookie的延时性,即设即用。

特性2:可以很方便的在cookie中存储数组,不需要一条条的写setcookie语句。

特性3:支持“.”索引,比如test.aaaa.bbbb是指cookie里的test[aaaa][bbbb]。

注意1:千万小心cookie的4kb限制,虽然有的现代浏览器可能扩充了这个限制。

注意2:cookie里存太多东西毕竟增加了网络数据传输以及服务器的数据处理。

function set_cookie($data,$path = '/',$time = 0,$doma = NULL)
{
	if(!is_array($data))
	{
		$para = func_get_args();
		$data = array($para[0] => $para[1]);
		$path = isset($para[2]) ? $para[2] : '/';
		$time = isset($para[3]) ? $para[3] : 0;
		$doma = isset($para[4]) ? $para[4] : NULL;
	}

	if(is_int($path))
	{
		$time = $path;
		$path = '/';
	}

	if(is_string($time))
	{
		$doma = $time;
		$time = 0;
	}

	$time = $time == 0 ? $time : time()+$time;

	foreach($data as $key => $value)
	{
		if(!is_array($value))
		{
			if(strpos($key,'.') === FALSE)
			{
				if(isset($_COOKIE[$key]))
				{
					if(is_array($_COOKIE[$key]))
					{
						$cookie_str = http_build_query(array($key => $_COOKIE[$key]));
						$cookie_arr = explode('&',$cookie_str);

						foreach($cookie_arr as $cookie)
						{
							$a_cookie = explode('=',$cookie);
							setcookie(urldecode($a_cookie[0]),NULL,-1,$path,$doma);
						}

						$_COOKIE[$key] = NULL;
					}
				}

				setcookie($key,$value,$time,$path,$doma);

				$_COOKIE[$key] = $value;
			}
			else
			{
				$cop = &$_COOKIE;
				$cox = substr_count($key,'.');

				foreach(explode('.',$key) as $ckk => $ckey)
				{
					if($ckk > 0)
					{
						$cookie_key .= '['.$ckey.']';
					}
					else
					{
						$cookie_key = $ckey;
					}

					if($ckk < $cox)
					{
						if(isset($cop[$ckey]))
						{
							if(!is_array($cop[$ckey]))
							{
								setcookie($cookie_key,NULL,-1,$path,$doma);

								$cop[$ckey] = NULL;
							}
						}
						else
						{
							$cop[$ckey] = NULL;
						}

						$cop = &$cop[$ckey];
					}
					else
					{
						if(isset($cop[$ckey]))
						{
							if(is_array($cop[$ckey]))
							{
								$cookie_str = http_build_query(array($cookie_key => $cop[$ckey]));
								$cookie_arr = explode('&',$cookie_str);

								foreach($cookie_arr as $cookie)
								{
									$a_cookie = explode('=',$cookie);
									setcookie(urldecode($a_cookie[0]),NULL,-1,$path,$doma);
								}

								$cop[$ckey] = NULL;
							}
						}
						else
						{
							$cop[$ckey] = NULL;
						}

						$cop = &$cop[$ckey];
					}
				}

				setcookie($cookie_key,$value,$time,$path,$doma);

				$cop = $value;
			}
		}
		else
		{
			$x_cookie_str = http_build_query($value);
			$x_cookie_arr = explode('&',$x_cookie_str);

			foreach($x_cookie_arr as $x_cookie)
			{
				$a_cookie = explode('=',$x_cookie);

				if(isset($a_cookie[1]))
				{
					set_cookie($key.'.'.str_replace(array('[',']'),array('.',''),urldecode($a_cookie[0])),urldecode($a_cookie[1]),$time,$path,$doma);
				}
			}
		}
	}
}

function cookie($key = NULL,$def = FALSE)
{
	if(!empty($key))
	{
		if(strpos($key,'.') === FALSE)
		{
			if(isset($_COOKIE[$key]))
			{
				return $_COOKIE[$key];
			}
			else
			{
				return $def;
			}
		}
		else
		{
			$cop = &$_COOKIE;

			foreach(explode('.',$key) as $ckey)
			{
				if(isset($cop[$ckey]))
				{
					$cop = &$cop[$ckey];
				}
				else
				{
					return $def;
				}
			}

			return $cop;
		}
	}
	else
	{
		return $_COOKIE;
	}
}

function unset_cookie($key = NULL,$path = '/',$doma = NULL)
{
	if(!empty($key))
	{
		if(strpos($key,'.') === FALSE)
		{
			if(isset($_COOKIE[$key]))
			{
				if(!is_array($_COOKIE[$key]))
				{
					setcookie($key,NULL,-1,$path,$doma);
				}
				else
				{
					$cookie_str = http_build_query(array($key => $_COOKIE[$key]));
					$cookie_arr = explode('&',$cookie_str);

					foreach($cookie_arr as $cookie)
					{
						$a_cookie = explode('=',$cookie);
						setcookie(urldecode($a_cookie[0]),NULL,-1,$path,$doma);
					}
				}

				unset($_COOKIE[$key]);
			}
		}
		else
		{
			$cop = &$_COOKIE;
			$ckeys = explode('.',$key);
			$pop_ckey = array_pop($ckeys);

			foreach($ckeys as $ckk => $ckey)
			{
				if($ckk > 0)
				{
					$cookie_key .= '['.$ckey.']';
				}
				else
				{
					$cookie_key = $ckey;
				}

				if(isset($cop[$ckey]))
				{
					$cop = &$cop[$ckey];
				}
				else
				{
					return;
				}
			}

			if(isset($cop[$pop_ckey]))
			{
				if(!is_array($cop[$pop_ckey]))
				{
					setcookie($cookie_key.'['.$pop_ckey.']',NULL,-1,$path,$doma);
				}
				else
				{
					$cookie_str = http_build_query(array($cookie_key.'['.$pop_ckey.']' => $cop[$pop_ckey]));
					$cookie_arr = explode('&',$cookie_str);

					foreach($cookie_arr as $cookie)
					{
						$a_cookie = explode('=',$cookie);
						setcookie(urldecode($a_cookie[0]),NULL,-1,$path,$doma);
					}
				}

				unset($cop[$pop_ckey]);
			}
		}
	}
	else
	{
		if(!empty($_COOKIE))
		{
			$cookie_str = http_build_query($_COOKIE);
			$cookie_arr = explode('&',$cookie_str);

			foreach($cookie_arr as $cookie)
			{
				$a_cookie = explode('=',$cookie);
				setcookie(urldecode($a_cookie[0]),NULL,-1,$path,$doma);
			}

			$_COOKIE = array();
		}
	}
}