個人原文:www.hijerry.cn/p/61701.htm…php
用戶認證就是判斷一個用戶是否爲合法用戶的過程。html
目前用戶認證大都是基於Cookie、Session實現的。對於HTTP協議還不熟悉的話,能夠參考《HTTP權威指南》。laravel
註冊、登錄幾乎是全部Web站點都具有的兩個功能。數據庫
以商城系統爲例,用戶輸入登陸名、密碼進行註冊、登錄,這樣系統內就能夠爲用戶保存如:購物車、訂單、商品喜愛等個性化信息。跨域
用戶認證的最主要目的是保存個性化信息。瀏覽器
用戶認證是用戶受權的基礎。以商城系統爲例,商家須要先進行用戶認證,系統才能判斷他是否有某個店鋪的管理權。緩存
API調用和網頁瀏覽同樣,也須要用戶認證。服務器
Session是一種將數據存儲在服務器端的會話控制技術,咱們可使用它實現用戶認證。微信
下面是一個基於Laravel5的PHP版本的用戶認證:session
/** * 用戶登陸 * @param string $login 登陸名 * @param string $password 登陸密碼 * @return UserModel|false */
function userLogin($login, $password) {
$user = UserModel::where('login', $login)->first();
if ($user && $user->checkPassword($password)) {
session()->put('_user', $user);
return $user;
} else {
return false;
}
}
/** * 獲取已經登陸的用戶實例 * @return UserModel|null */
function getLoginUser() {
return session()->get('_user');
}
複製代碼
userLogin
函數接受用戶名、密碼兩個參數進行用戶認證工做,認證成功返回用戶實例
,失敗返回false
。
getLoginUser
函數用於獲取已經登陸的用戶,已登陸返回用戶實例
,未登陸返回null
(由session()->get函數返回的)。
第8行:按$login
從數據庫中取出匹配的第一個用戶實例
。
第9行:判斷是否定證成功,checkPassword
用於判斷$password
是否符合$user
的密碼。
第10行:將$user
存入session中,鍵爲_user
。
第11行:認證成功,返回用戶實例$user
。
第13行:認證失敗,返回false
。
第22行:從session中取出用戶實例。
這種作法的核心思想是把用戶數據直接交由Session保管。
Session能夠基於Cookie或URL實現,不論哪一種形式,都須要先由服務器種下session-id
(種在Cookie裏或是重在URL裏),後續請求帶上這個session-id,服務器才能實現Session。
API請求大多會使用HTTP Client完成,它是不帶瀏覽器的Cookie(除非手動設置)。同時,API請求大都都只有一個請求和一個響應,session-id是來不及種的。
基於令牌的用戶認證,本質是將登陸時隨機生成的token
寫在HTTP頭或是寫在URL上,服務器經過鑑別token
來進行用戶認證。
上代碼:
/** * 用戶登陸 * @param string $login 登陸名 * @param string $password 登陸密碼 * @return UserModel|false */
function userLogin($login, $password) {
$user = UserModel::where('login', $login)->first();
if ($user && $user->checkPassword($password)) {
$token = $user->generateAuthToken();
session()->put('_token', $token);
cache()->put('user_' . $token, $user);
return $user;
} else {
return false;
}
}
/** * 獲取已經登陸的用戶實例 * @return UserModel|null */
function getLoginUser($token = null) {
if (! $token) $token = session()->get('_token');
$cache_key = 'user_' . $token;
return cache()->get($cache_key);
}
複製代碼
這個版本的userLogin
函數,在認證成功後,經過用戶實例生成一個token
放入session,再把用戶實例$user
放入緩存系統中(如Redis、Memcache)。token
通常都是32位的md5值。
getLoginUser
函數也有所變化,它能夠接受指定的$token
來獲取用戶實例,默認狀況下它會從session中取出token。
第10~12行:使用$user
生成token
,將用戶實例存入緩存系統中。
第24~26行:使用token
從緩存系統中獲取用戶實例。
的一種可用的用於生成token
的方法:
/** * 生成認證token * @return string 認證token */
public function generateAuthToken() {
if ($this->token) return $this-token;
return $this->token = md5(md5($this->id . time()));
}
複製代碼
time()
函數返回當前unix時間戳。能夠看到,token與用戶id
和登陸時間
有關,這能夠保證惟一性。
這樣的用戶認證下,API請求怎麼作呢?
咱們先建立一個接口 /login
用於登陸,接口的返回值裏,附上登陸成功後的 token
,HTTP Client將這個token緩存起來,在以後的請求中帶上這個token便可。這樣以來,用戶認證就不是基於Cookie而是基於token了。
這樣的用戶認證已經能夠知足大部分應用場景瞭如Cookie失效、API請求和統一認證。但還有一個場景沒法知足,那就是多終端數據共享。好比用戶在電腦上登陸了一次,在手機上登陸了一次,系統會生成2個token,這兩個token對應的用戶實例是不同的,因此用戶在電腦上設置的個性化信息(好比性別,名稱)沒法共享到手機上。
多終端共享須要明確兩點:
實現多終端數據共享還有其餘方法,下面舉例一個我在項目中用的方法。
代碼以下:
/** * 用戶登陸 * @param string $login 登陸名 * @param string $password 登陸密碼 * @return UserModel|false */
function userLogin($login, $password) {
$user = UserModel::where('login', $login)->first();
if ($user && $user->checkPassword($password)) {
$token = $user->generateAuthToken();
session()->put('_token', $token);
// 認證
cache()->put('user_token_' . $token, $user->id);
// 數據
cache()->put('user_' . $user->id, $user);
return $user;
} else {
return false;
}
}
/** * 獲取已經登陸的用戶實例 * @return UserModel|null */
function getLoginUser($token = null) {
if (! $token) $token = session()->get('_token');
$token_cache_key = 'user_token_' . $token;
$user_id = cache()->get($token_cache_key);
if (! $user_id) return null; // token失效,認證過時
$user_cache_key = 'user_' . $user_id;
$user = cache()->get($user_cache_key);
if (! $user) {
// 緩存失效,從新緩存
$user = UserModel::find($user_id);
cache()->put($user_cache_key, $user);
}
return $user;
}
複製代碼
這種認證方式下,token
只能解析出user_id
,這就比如是一個用戶指針,系統再由user_id
解析出用戶實例
。這樣能夠保證,不一樣終端拿到不一樣的token,這些token的過時時間不會相互影響,而不一樣token能夠拿到同一個用戶數據,從而實現多終端用戶數據共享。
getLoginUser
函數,先檢查token
是否失效,再進一步檢查用戶實例
緩存是否失效。
多終端數據共享的應用場景也很普遍,好比帳號激活,發一份Email郵件,讓用戶點擊連接進行帳號激活。在激活操做裏,系統須要知道用戶想要激活那個帳號,一個一般的作法以下:
/** * 生成用於激活帳號的連接 * @return string 用於激活的uri */
function generateActivateLink() {
$code = md5('activate' . Auth::id() . time());
cache()->put($code, Auth::id());
return url('/user/activate?code=' . $code);
}
/** * 激活用戶 * @param string $code 激活碼 * @return string 用於激活的uri */
function activateUser($code) {
$user_id = cache()->get($code);
if (! $user_id) return false;
// 修改數據庫
$user = UserModel::find($user_id);
$user->status = UserModel::STATUS_ACTIVATED;
$user->save();
// 修改緩存
$user_cache_key = 'user_' . $user_id;
if (cache()->get($user_cache_key)) {
cache()->put($user_cache_key, $user);
}
return $user;
}
複製代碼
能夠看到,生成的激活連接中的code
實際上是緩存鍵,使用code
能夠獲取到用戶id
,這樣系統就知道了須要激活哪一個用戶。
在激活時,系統只須要修改緩存中的用戶實例便可,用戶不須要從新登陸帳號以刷新緩存中的數據。
第8行:url()
函數,是laravel中用於生成完整url的函數。
第21行:修改用戶的status
字段值爲STATUS_ACTIVATED
對應的值。
第22行:保存修改的信息到數據庫。
OAuth協議可讓第三方在不知道用戶敏感信息的前提下,獲取服務器內用戶的資源。第三方登陸就可使用OAuth協議來完成,如微信、QQ、微博等社交平臺都提供第三方登陸接入服務。
OAuth2.0的受權能夠簡單分爲三步:
第一步,又稱用戶登陸引導頁面。在微信登陸時,這個頁面的域名是在微信下的,用戶贊成受權後,微信會把受權碼Code送到服務器(經過回調URI的形式)。拿到這個Code表示用戶贊成了受權
。
第二步,在微信登陸時,這個token又叫access_token
。拿到這個Token表示服務器是合法的
。
第三步,在微信登陸時,這一步能夠拿到用戶的open_id
。
在微信登陸中,若是要獲取用戶基本信息,須要用open_id
+access_token
才能獲得。
關於OAuth2.0協議更多內容,能夠參考這2篇文章:深刻理解OAuth2.0協議 ,理解OAuth 2.0
一個用戶能夠"綁定"多個第三方帳號,這是一個比較好的處理第三方用戶的方式。第三方用戶的管理必須重視,若是管理混亂,綁定的信息不能指向同一個用戶,就會出現多身份問題,好比用戶使用手機登陸購買的東西,在使用微信登陸時卻提示沒有購買。
我介紹一下個人作法,數據庫兩張表:
user
表,記錄用戶信息。這裏有telephone
和email
等可用於登陸的字段user_third
表,記錄用戶綁定的第三方帳號信息。登陸邏輯以下:
user
表裏查詢信息。user_third
裏查詢信息,若是未找到,則在user
表裏新建用戶,再將第三方帳號信息保存到user_third
裏,最後把新建的用戶與第三方帳號信息綁定;若是能找到,則返回第三方帳號所綁定的user
表裏的數據。這種作法,能夠保證用戶數據均來自user
表,就不會有多身份問題,同時一個用戶也能夠綁定多個第三方帳號,更加便於管理。
還有一種狀況是綁定信息衝突,好比用戶第一個帳號綁定了手機號和微信帳號,過段時間後,他用QQ帳號登陸時(此時這個QQ號沒有對應系統內的用戶)系統會建立第二個帳號,此時他再去綁定手機號或微信號的時候,會由於user
表的telephone
字段、user_third
表中已有信息,而致使綁定失敗。
處理這種狀況經常使用的方法是解綁,用戶能夠解綁QQ號,再綁定QQ號至第一次建立的帳號;也能夠選擇解綁手機、微信,再將手機、微信綁到第二個帳號上。
單點登陸(Single Sign On,SSO)經常使用於多服務器共存的大型網站,即一次用戶認證,便可訪問旗下全部網站。
以豆瓣網爲例,它有豆瓣讀書、豆瓣電影子網站,這兩個子網站部署在不一樣服務器上。
首先,用戶數據不能放在Session裏,因此基於Token的認證方式很快進入咱們的視野,也就是版本2和版本3的認證方式。須要注意的是,不一樣服務器必須使用同一個緩存系統。能夠單獨起一個服務器用做數據存儲。這樣一來,系統均可以根據token
從緩存系統中解析出用戶實例
。
仔細的同窗會發現,版本3的token
是存在Session裏的,就算在子網A中登陸完了,在子網B的Session中並無這個token
。一個常見的作法是共享Cookie,讓子網A的Cookie可讓子網B使用,再將token
放在Cookie中,而不是放在Session裏。
例如豆瓣讀書域名爲:book.douban.com,豆瓣電影域名爲:movie.douban.com,如今要種一個Cookie,使得這兩個域名都能使用。由於他們是屬於同一個二級域名douban.com
下的,因此可讓用戶在域名www.douban.com下登陸,把Cookie的路徑設置爲.douban.com
,便可實現Cookie的共享。
若是遇到www.taobao.com和www.douban.com要作統一身份認證怎麼辦呢?由於沒有共同的二級域名,因此將認證系統建於第三個網站中,這個網站也叫統一認證網站(簡稱認證網)。
咱們先假設一個未登陸的用戶。
/home
網頁,網站A檢測出用戶未登陸,因而使用HTTP重定向,引導用於至認證網的登陸頁面去。/home
頁面,可是帶上了token
。接收這次響應後,瀏覽器已有了認證網的Cookie,因此用戶在認證網處於登陸狀態。token
參數,並保存起來。在響應中,種下網站A的Cookie。此時用戶在網站A也處於登陸狀態。咱們假設這個已經認證過的用戶,去訪問網站B。
能夠看到,在引導用戶至認證網的登陸頁面時,由於用戶在認證網處於登陸狀態,因此認證網直接重定向到網站B的/profile
頁面。
有朋友會發現,認證網的功能其實能夠融合到網站A或網站B中。確實能夠這樣作,可是不推薦,由於要秉持低耦合的原則,將認證系統獨立出來會更加方便使用和管理。
進一步理解,使用OAuth協議也能夠實現單點登陸功能,它就是API版本的單點登陸。
在基於令牌的認證裏,token
是最爲關鍵的信息,若是有第三方竊取到了用戶的token,他就能夠冒充用戶的進行操做。
啥意思呢?就是把token
放在HTTP頭裏,儘可能讓用戶感受不到token
的存在。好比下面的HTTP頭:
...
X-AUTH-TOKEN: 340c6f730612769b71075d4fbbe5d337
...
複製代碼
可是若是HTTP包被黑客獲取,他仍然可以竊取到token
。
HTTPS會將數據包加密,因此黑客就算截取到數據包到也沒法獲取token
。
文章內容是本身結合理論,在實踐中總結出來的,歡迎你們留言交流、討論~~