【Web總結】用戶認證

個人原文:www.hijerry.cn/p/61701.htm…php

前言

用戶認證就是判斷一個用戶是否爲合法用戶的過程。html

目前用戶認證大都是基於Cookie、Session實現的。對於HTTP協議還不熟悉的話,能夠參考《HTTP權威指南》。laravel

應用場景

註冊、登錄幾乎是全部Web站點都具有的兩個功能。數據庫

以商城系統爲例,用戶輸入登陸名、密碼進行註冊、登錄,這樣系統內就能夠爲用戶保存如:購物車、訂單、商品喜愛等個性化信息。跨域

用戶認證的最主要目的是保存個性化信息。瀏覽器

用戶認證是用戶受權的基礎。以商城系統爲例,商家須要先進行用戶認證,系統才能判斷他是否有某個店鋪的管理權。緩存

API調用和網頁瀏覽同樣,也須要用戶認證。服務器

版本1:基於Session

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。

版本2:基於令牌Token

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對應的用戶實例是不同的,因此用戶在電腦上設置的個性化信息(好比性別,名稱)沒法共享到手機上。

版本3:多終端數據共享

多終端共享須要明確兩點:

  • 各個終端的登陸時長互不影響
  • 各個終端的用戶數據一致

實現多終端數據共享還有其餘方法,下面舉例一個我在項目中用的方法。

代碼以下:

/** * 用戶登陸 * @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協議可讓第三方在不知道用戶敏感信息的前提下,獲取服務器內用戶的資源。第三方登陸就可使用OAuth協議來完成,如微信、QQ、微博等社交平臺都提供第三方登陸接入服務。

OAuth2.0

OAuth2.0的受權能夠簡單分爲三步:

  1. 獲取用戶受權碼Code
  2. 獲取用戶受權令牌Token
  3. 使用受權令牌Token獲取用戶信息

第一步,又稱用戶登陸引導頁面。在微信登陸時,這個頁面的域名是在微信下的,用戶贊成受權後,微信會把受權碼Code送到服務器(經過回調URI的形式)。拿到這個Code表示用戶贊成了受權

第二步,在微信登陸時,這個token又叫access_token。拿到這個Token表示服務器是合法的

第三步,在微信登陸時,這一步能夠拿到用戶的open_id

在微信登陸中,若是要獲取用戶基本信息,須要用open_id+access_token才能獲得。

關於OAuth2.0協議更多內容,能夠參考這2篇文章:深刻理解OAuth2.0協議理解OAuth 2.0

如何集成

一個用戶能夠"綁定"多個第三方帳號,這是一個比較好的處理第三方用戶的方式。第三方用戶的管理必須重視,若是管理混亂,綁定的信息不能指向同一個用戶,就會出現多身份問題,好比用戶使用手機登陸購買的東西,在使用微信登陸時卻提示沒有購買。

我介紹一下個人作法,數據庫兩張表:

  • user表,記錄用戶信息。這裏有telephoneemail等可用於登陸的字段
  • user_third表,記錄用戶綁定的第三方帳號信息。

登陸邏輯以下:

  • 當用戶使用如手機號、郵箱、登陸名登陸時,在user表裏查詢信息。
  • 當用戶使用第三方登陸時,系統先去user_third裏查詢信息,若是未找到,則在user表裏新建用戶,再將第三方帳號信息保存到user_third裏,最後把新建的用戶與第三方帳號信息綁定;若是能找到,則返回第三方帳號所綁定的user表裏的數據。

這種作法,能夠保證用戶數據均來自user表,就不會有多身份問題,同時一個用戶也能夠綁定多個第三方帳號,更加便於管理。

還有一種狀況是綁定信息衝突,好比用戶第一個帳號綁定了手機號和微信帳號,過段時間後,他用QQ帳號登陸時(此時這個QQ號沒有對應系統內的用戶)系統會建立第二個帳號,此時他再去綁定手機號或微信號的時候,會由於user表的telephone字段、user_third表中已有信息,而致使綁定失敗。

處理這種狀況經常使用的方法是解綁,用戶能夠解綁QQ號,再綁定QQ號至第一次建立的帳號;也能夠選擇解綁手機、微信,再將手機、微信綁到第二個帳號上。

單點登陸

單點登陸(Single Sign On,SSO)經常使用於多服務器共存的大型網站,即一次用戶認證,便可訪問旗下全部網站。

豆瓣網爲例,它有豆瓣讀書豆瓣電影子網站,這兩個子網站部署在不一樣服務器上。

基於Token的認證

首先,用戶數據不能放在Session裏,因此基於Token的認證方式很快進入咱們的視野,也就是版本2和版本3的認證方式。須要注意的是,不一樣服務器必須使用同一個緩存系統。能夠單獨起一個服務器用做數據存儲。這樣一來,系統均可以根據token從緩存系統中解析出用戶實例

同源:共享Cookie

仔細的同窗會發現,版本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要作統一身份認證怎麼辦呢?由於沒有共同的二級域名,因此將認證系統建於第三個網站中,這個網站也叫統一認證網站(簡稱認證網)。

mark

咱們先假設一個未登陸的用戶。

  1. 第一次請求。請求網站A的/home網頁,網站A檢測出用戶未登陸,因而使用HTTP重定向,引導用於至認證網的登陸頁面去。
  2. 第二次請求。這是由瀏覽器自主發起的,認證網響應出登陸頁面。
  3. 第三次請求。用戶輸入帳號密碼進行登陸,服務器認證成功後,種下Cookie,並重定向至網站的A的/home頁面,可是帶上了token。接收這次響應後,瀏覽器已有了認證網的Cookie,因此用戶在認證網處於登陸狀態。
  4. 第四次請求。瀏覽器自主發起的,網站A必須識別出token參數,並保存起來。在響應中,種下網站A的Cookie。此時用戶在網站A也處於登陸狀態。

咱們假設這個已經認證過的用戶,去訪問網站B。

mark

能夠看到,在引導用戶至認證網的登陸頁面時,由於用戶在認證網處於登陸狀態,因此認證網直接重定向到網站B的/profile頁面。

有朋友會發現,認證網的功能其實能夠融合到網站A或網站B中。確實能夠這樣作,可是不推薦,由於要秉持低耦合的原則,將認證系統獨立出來會更加方便使用和管理。

進一步理解,使用OAuth協議也能夠實現單點登陸功能,它就是API版本的單點登陸。

令牌管理

在基於令牌的認證裏,token是最爲關鍵的信息,若是有第三方竊取到了用戶的token,他就能夠冒充用戶的進行操做。

隱藏Token

啥意思呢?就是把token放在HTTP頭裏,儘可能讓用戶感受不到token的存在。好比下面的HTTP頭:

...
X-AUTH-TOKEN: 340c6f730612769b71075d4fbbe5d337 
...
複製代碼

可是若是HTTP包被黑客獲取,他仍然可以竊取到token

使用HTTPS

HTTPS會將數據包加密,因此黑客就算截取到數據包到也沒法獲取token

文章內容是本身結合理論,在實踐中總結出來的,歡迎你們留言交流、討論~~

相關文章
相關標籤/搜索