【Web總結】使用者認證

JerryCheese發表於2018-07-16

我的原文:www.hijerry.cn/p/61701.htm…

前言

使用者認證就是判斷一個使用者是否為合法使用者的過程。

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

應用場景

註冊、登陸幾乎是所有Web站點都具備的兩個功能。

以商城系統為例,使用者輸入登入名、密碼進行註冊、登陸,這樣系統內就可以為使用者儲存如:購物車、訂單、商品喜好等個性化資訊。

使用者認證的最主要目的是儲存個性化資訊。

使用者認證是使用者授權的基礎。以商城系統為例,商家需要先進行使用者認證,系統才能判斷他是否有某個店鋪的管理權。

API呼叫和網頁瀏覽一樣,也需要使用者認證。

版本1:基於Session

Session是一種將資料儲存在伺服器端的會話控制技術,我們可以使用它實現使用者認證。

下面是一個基於Laravel5的PHP版本的使用者認證:

/**
 * 使用者登入
 * @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

文章內容是自己結合理論,在實踐中總結出來的,歡迎大家留言交流、討論~~

相關文章