一、概述
在使用瀏覽器登錄某個系統時,我們經常會看到“記住登錄”這個選項,一般我們會認爲記住登錄就是下次重新訪問這個界面時,不需要再次輸入用戶名和密碼,直接就進入系統。有的人會覺得這樣做不安全,認爲是自己的用戶名和密碼被記住了,但是他也不知道是怎麼被記住,並且記在哪兒。懂點web技術的同學知道是瀏覽器通過cookie保存了登錄憑證,下次登錄時瀏覽器會自動提交憑證,而對於使用者來說整個驗證過程都是透明的,因爲瀏覽器自動重定位頁面速度非常快,一般人會覺得似乎沒有訪問過登錄界面。下面我就詳細地給大家介紹一下記住登錄的整個工作原理以及實現代碼,希望大家能夠對“記住登錄”有一個清楚的認識,做好個人賬戶安全管理工作。
B/S(瀏覽器/服務器)應用也就是我們常說的web應用,一般通過瀏覽器進行使用,也有一些應用採用內嵌瀏覽器內核的方式提供給用戶使用,這些都是一些直觀上的感受。其核心在於使用HTTP協議進行信息交互的應用。
HTTP協議全稱爲超文本傳輸協議,包括1.1版本及之前的版本都是採用字符編碼的報文,通信過程採用一問一答的方式,如下圖所示,請求由客戶端發出,服務端返回響應信息,之後如果還要進行通信,還是由客戶端發出請求,服務端再返回新的請求。對於服務器和客戶端來說,每一次的通信過程都是新的,中間不保留任何狀態信息,也正是因爲如此,大量的事務可以得到高效的處理。這就是HTTP無狀態協議本質所在。
因爲早期的業務場景比較簡單,但是隨着後來業務的發展,必須要有一種機制,記住上一次通信狀態信息,比如典型的用戶管理系統,首次登錄時用戶輸入用戶名和密碼進行驗證,驗證通過後就需要記住登錄狀態信息,避免在下次通信時重複驗證過程。因此,HTTP協議中就引入了Cookie機制,即在每次通信的報文中加入一個Cookie字段,用於標識客戶端身份。
首次設置Cookie時,服務器會在報文頭部加入Set-Cookie字段,同時,服務器會在自己的會話Session中記錄下這個Cookie:
Set-Cookie: PHPSESSID=27r66cdl08m4jlmk7laufqddm0; path=/
Cookie各個屬性說明
客戶端拿到這個Cookie後,在之後的整個請求報文中,都會加入Cookie字段:
Cookie: PHPSESSID=27r66cdl08m4jlmk7laufqddm0
我們可以通過瀏覽器開發人員工具進行查看:
服務端收到客戶端的報文後,會自動從報文頭部中提取出Cookie,並整理彙總與之關聯的Session數據。
這裏需要注意的是:
1.服務端會爲每個客戶端維護一份狀態信息,叫做Session,客戶端也針對每一個服務器維護一份狀態信息,叫做Cookie。他們之間會通過一個唯一的Cookie值進行關聯起來,比如本示例中的PHPSESSID。
2.服務器通過Set-Cookie來設置更新客戶端的Cookie,但並不意味着雙方的信息完全同步。一方面,服務器更新了自己的Seesion之後需要在報文頭部加入Set-Cookie才能夠更新客戶端的Cookie,另一方面,我們可以手動清除客戶端的Cookie,或者使用工具私自僞造修改Cookie。所以,這也是Web安全漏洞所在。
3.PHPSESSID是PHP引擎自動設置的一個Cookie,是全局唯一標識,標識與其通信的客戶端,我們在PHP腳本中設置的全部Cookie都會與這個唯一標識對應起來,也就是說,當一個客戶端請求過來時,引擎會自動的提取Cookie中的PHPSESSID,再從自己的Session中獲取與之關聯的Session值,初始化PHP腳本執行的全局環境,而作爲腳本開發人員來說,無需關心哪個客戶端對應那些Session。其實,所有的CGI程序都是如此。
二、登錄狀態信息維護
當我們訪問需要登錄的頁面(URL)時,服務器會檢查當前客戶端是否登錄,具體的做法是判斷Session中記錄登錄狀態的字段是否有效,如果有效就直接返回用戶所要請求的頁面;如果無效就強制客戶端跳轉到登錄頁面。
填寫用戶名和密碼,向服務器提交請求。
服務器收到請求後,會從報文中拿到用戶名和密碼,並查詢數據庫驗證其是否一致。
驗證通過後,服務器會在當前的會話中記錄下客戶端已登錄的信息,並返回用戶之前請求的頁面。
此後的通信過程,只要涉及到需要用戶在登錄後訪問的頁面,服務器都會從它自己的Seesion中檢查用戶的登錄情況,而這Session與客戶端的一一對應關係是由唯一的Cookie,在本示例中是PHPSESSID來實現的。
一般地,這個由CGI程序(PHP引擎)設置的Cookie有效期爲0,也就是當瀏覽器關閉後Cookie自動失效清除,重新打開瀏覽器發送請求,服務端會重新分配一個Session,因此當我們關閉瀏覽器並重新打開訪問頁面時,就需要再次進行登錄,這也就有了“記住登錄”這個由來。
當然你也可以設置它的過期時間,PHP引擎的具體配置在php.ini文件中,下面給出Session的全部設置參數:
[Session]
session.save_handler = files
;session.save_path = "N;MODE;/path"
;session.save_path = "/tmp"
session.use_cookies = 1
;session.cookie_secure =
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
;session.entropy_length = 32
;session.entropy_file = /dev/urandom
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 0
session.hash_bits_per_character = 5
session.save_path="D:\phpStudy\tmp\tmp"
;session.upload_progress.enabled = On
;session.upload_progress.cleanup = On
;session.upload_progress.prefix = "upload_progress_"
;session.upload_progress.name =
;session.upload_progress.freq = "1%"
;session.upload_progress.min_freq = "1"
三、記住登錄
記住登錄主要爲避免以下兩種情況:
1.爲了避免每次打開瀏覽器時都需要進行再次登錄,我們就需要使用自己設置的Cookie進行記錄,本次示例中有效時間設置爲7,7之後可選擇重新進行登錄。
2.此外,服務器端的會話Seesion保存時間也是有要求的,PHP默認設置爲24分鐘,在php.ini配置文件中可以找到:
session.gc_maxlifetime = 1440
也就說在24分鐘內,客戶端沒有向服務端發送請求,24分鐘後,服務器會自動清除當前會話信息,那麼當前的登錄信息也會失效,用戶必須重新進行登錄。
因此我們在客戶端保存一個自己分配的Cookie字段uid和remember_me
Cookie: PHPSESSID=cstlvkaq88a18d1dg63k4dccs5; remember_me=be59b7e7173438fe512004e238d66fd5
uid是用戶的唯一id,remember_me是臨時隨機分配的128位字符串。
當前會話失效後,我們通過客戶端Cookie中的uid和remember_me從數據庫中查詢用戶的相關信息。
如果查找失敗,就需要用戶重新進行登錄,跳轉到登錄頁面。
如果查找成功,就說明用戶已經的登錄,爲了安全起見,我們需要重新分配一個remember_me,並將其保存到數據庫中,返回用戶要訪問的頁面。整個過程如果執行的非常快,用戶是察覺不到的,就好像自己直接通過驗證並訪問所需要的頁面。
四、代碼實現
對於所有需要登錄的頁面,我們統一使用一個基類控制器(基於ThinkPHP3.2框架)。
<?php
/**
* BaseController(Admin\Controller\BaseController.class.php)
*
* 功 能:後臺首頁基類控制器
*
* 作 者:李康
* 完成時間:2018/04/04
* 修 改:2018/04/04 增加了ajaxReturn,處理客戶端的異步請求
* 2018/04/20 增加了ajax請求的未登錄攔截處理
* 系統自動更新會話錯誤修復
*
*/
namespace Admin\Controller;
use Think\Controller;
class BaseController extends Controller {
public $uid = 0;
public function __construct()
{
$uid = $this->checkLogin();
if($uid) {
$this->uid = $uid;
} else {
if (IS_AJAX) {
header('HTTP/1.1 404');
$this->error('unlogin');
} else {
$this->redirect('Admin/Login/index');
}
}
parent::__construct();
}
/**
* 檢測用戶是否登錄
*
* @return bool :true,已經登錄;false,未登錄
*/
public function checkLogin()
{
// 獲取服務器端的uid
$uid = $_SESSION['uid'];
if ($uid) {
return $uid;
}
// 1 用戶是否選擇了記住登錄
// 1.1 如果用戶沒有選擇記住登錄,跳轉到登錄界面
// 1.2 如果用戶選擇了記住登錄,就要查詢數據庫,看客戶端cookie中的token是否和數據庫中的一致
// 獲取客戶端的rememberme
$rememberMe = cookie('remember_me');
if (!$rememberMe) {
return false;
}
// 1 去數據庫中查詢會話token
// 1.1 如果數據庫中存在
// 1.1.1 獲取用戶信息,更新會話token,重寫入服務器session,更新客戶端的cookie
// 1.1.2 將更新後的cookie寫入數據庫
// 1.2 不存在,返回false
// 去數據庫中查詢會話token
$userid = cookie('uid');
$userModel = M('user');
$user = $userModel->field('id, password')->where("id='{$userid}' AND loginSessionId='{$rememberMe}'")->find();
if ($user) {
// 更新會話token
$remember_me = md5($user->password.time());
// 寫入會話
session('uid', $user['id']);
session('uname', $user['nickname']);
session('uavatar', $user['avatar']);
session('uroles', $user['roles']);
// 更新客戶端的cookie
cookie('remember_me', $remember_me, 3600*24*7);
// 將更新後的remember_me
$userModel->where("id={$user['id']}")->setField('loginSessionId', $remember_me);
} else {
return false;
}
}
}
登錄過程處理
<?php
/**
* LoginController(Admin\Controller\BaseController.class.php)
*
* 功 能:後臺首頁登錄控制器
*
* 作 者:李康
* 完成時間:2018/04/04
* 修 改:2018/04/04 增加了ajaxReturn,處理客戶端的異步請求
* 2018/04/19 修改了登錄成功後和退出登錄 跳轉URL處理
* 服務端會話中加入nickname、roles、avatar信息暫存
*
*/
namespace Admin\Controller;
use Think\Controller;
class LoginController extends Controller {
/**
* 檢測用戶是否登錄
*
* @return bool :true,已經登錄;false,未登錄
*/
public function index()
{
$goto = I('get.goto', '/');
$uid = $this->checkLogin();
if($uid) {
$this->success('用戶已登錄', $goto, 3);
} else {
$this->assign('goto', $goto);
$this->display();
}
}
public function ajax(){
$this->display();
}
public function login()
{
$username = I('post.username', '');
$password = I('post.password', '');
$remember_me = I('post.remember_me', '');
$goto = I('post.goto', '/');
// 校驗參數
if (empty($username) || empty($password)) {
$this->error('用戶名或密碼不能爲空');
}
// 用戶是否存在
$userModel = M('user');
$user = $userModel->field('id, nickname, password, salt, locked, roles, avatar')->where("`nickname`='$username'")->find();
// 用戶不存在
if (!$user) {
$this->ajaxReturn(array(
'success' => false,
'message' => '用戶不存在'
));
}
// 用戶是否被禁用
if ($user['locked'] > 0) {
$this->ajaxReturn(array(
'success' => false,
'message' => '用戶被禁用'
));
}
// 用戶名和密碼是否一致
$password = md5($user['nickname'].$password.$user['salt']);
if ($password != $user['password']) {
$this->ajaxReturn(array(
'success' => false,
'message' => '密碼錯誤'
));
}
// 寫入會話
session('uid', $user['id']);
session('uname', $user['nickname']);
session('uroles', $user['roles']);
session('uavatar', $user['avatar']);
cookie('uid', $user['id'], 3600*24*7);
// 是否記住登錄
if ($remember_me) {
$remember_me = md5($password.time());
cookie('remember_me', $remember_me, 3600*24*7);
$userModel->where("id={$user['id']}")->setField('loginSessionId', $remember_me);
}
// 記錄日誌
saveLog($user['id'], 'login', 'login', "用戶{$username}在".date('Y-m-d H:i:s')."登錄系統");
$this->ajaxReturn(array(
'success' => true,
'message' => '驗證通過',
'goto' => $goto
));
}
public function logout()
{
$goto = I('request.goto', '/Admin/Login/index');
session('uid', null);
cookie('uid', null);
$this->redirect($goto);
}
/**
* 檢測用戶是否登錄
*
* @return bool :true,已經登錄;false,未登錄
*/
private function checkLogin()
{
// 獲取服務器端的uid
$uid = $_SESSION['uid'];
if ($uid == null) {
return false;
}
// 獲取客戶端的userid
$userid = cookie('uid');
if ($userid == $uid) {
return $uid;
} else {
return false;
}
}
}
源碼地址:https://github.com/liebertLEOS/edu
希望本篇文章可以給新手一些指導,如有錯誤或不準確的地方,請在文末留言!