令牌Token和會話Session原理與攻略

朝野佈告發表於2019-05-15

        本篇文章將從無到完整的登入框架或API詳細講述登入令牌原理、攻略等安全點。

        有些協議或框架也喜歡把令牌叫票據(Ticket),不論是APP還是Web瀏覽器,很多框架或協議都用到到了本文所說的這套類似的認證機制(客戶端各種加密使用者名稱密碼當我沒說),這裡的以Asp.net core下Web登入和驗證為例子進行講述,但原理攻略和語言、框架都無關。

 

目錄:

一、過程與原理

二、Demo資料庫結構

三、Demo原始碼介紹

四、構建與驗證Token

五、Token失效與登入唯一性

六、CAS/SSO單點登入

七、URL授權驗證與掃碼登入

八、Session實現

九、關於Token重新整理

 

本片文章Demo:https://github.com/chaoyebugao/AcctAuthDemo

 

 

一、過程與原理 

令牌授權過程
 

令牌機制簡單過程(點選檢視大圖)

        首先,這套機制使用場景是登入授權和身份驗證,可以用在Web上,也可以用在API的訪問控制上。這套機制其實和很多無狀態框架登入/授權驗證協議類似,這裡將的其實和OAuth2.0裡面授權碼模式的原理是一樣的(authorization code),只不過我們在這裡將其步驟拆分,瞭解其原理和實現,以後搭建專案應用才能庖丁解牛。還有一點,很多框架的授權機制都太繁重且並不能靈活應用,這時候就可以自己搭一個。

        首先,使用者使用終端向伺服器提供可信憑證(一般登入是使用者名稱密碼,微信公眾平臺是appid+appsecret),服務端確認憑證正確,則返回授權的令牌(以下稱Token)。這個Token是隨機的字串且與本次授權唯一相關。返回Token給終端的同時服務端也要一併儲存Token,這樣終端和服務端都只認Token,終端所有請求傳送都需要攜帶此Token,服務端會驗證和控制此Token。此時Token就有兩個,一個是終端Token,一個是服務端Token,其中一個不對或沒有,服務端都是拒絕的。

        舉個例子,你上12306購票,購買過程就是授權你Token的過程,你的紙質票就是Token,另外一半對應的Token儲存在12306那的DB裡頭,所有門閘就是閘道器,當你過門閘時會驗證你Token是否對應DB的Token。你下車後,12306就把DB的Token標記處理掉,這樣服務端就不會再認你手上的紙質票,票也就作廢了。

        圍繞這一機制,我們將講述CAS單點登入、令牌授權與身份驗證、Session實現、防重放攻擊、登入唯一性、URL授權驗證(用於驗證郵箱等)等

 

 

二、Demo資料庫結構

裝置表:用於識別、記錄不同的裝置,同一裝置應該有唯一的標記Id,下面詳說

令牌表:用於持久化令牌,ExpireAt為過期時間,Token即令牌字串,根據UserId與使用者表相關聯,根據DeviceId與裝置表相關聯

使用者表:使用者表,儲存使用者名稱密碼等

裝置表和裝置標記(DeviceId)是可有可無的,可以根據實際業務來處理,有必要的話再增加其他相關聯的資料和表

 

 

三、Demo原始碼介紹

使用者Controller 

HomeController

UserController - 使用者註冊、登入、登出登入

HomeController - Index - 預設啟動頁,Token驗證頁

 

 

四、構建與驗證Token

構建Token

驗證Token

        Token的構建發生在使用者提供的憑證(如使用者名稱密碼)被服務端確認無誤之後。一次登入/授權的Token分兩部分,服務端持有的我們叫資料庫Token,使用者端(Endpoint)持有的叫終端Token。終端Token可以是任意的隨機字串構成,所以這裡最後要根據登入情況來求得雜湊值即終端Token本身。因為後面要根據終端Token來查詢處理資料庫Token記錄,所以他們必須有種關聯,這種關聯就是如上圖所示,終端Token+裝置Id得到的雜湊值即資料庫Token本身。

        可以看出,整個生成過程是單向不可逆的,驗證也只能是單向驗證,所以生成關係是這樣的:

授權Token構建關係圖

授權Token構建關係圖

 

        這裡有幾點要注意的:

  • 終端Token應該有足夠的長度,且每次應隨機生成,因此才有Guid.NewGuid()參與求值
  • 終端Token參與生成的userId、name和inputPassword是起到了鹽作用,讓整個構建更加複雜
  • 不論是終端Token還是資料庫Token都不應該可逆加密處理任何內容,因為可解密的話不論是終端還是資料庫資料洩露的,都有被破解的風險,所以用雜湊求值是最合適的
  • 構建資料庫Token有deviceId參與,這樣每次Token就只能是對應的deviceId才能被驗證,這樣就起了繫結作用。除了deviceId還可以繫結其他場景相關的,比如IP地址、終端型別
  • 日誌最好不要記錄任何Token

        兩部分Token構建好之後,終端Token將被返回給終端,資料庫Token持久化到服務端中。終端和資料庫都要將各自的Token和場景資訊持久化,Demo裡面終端Token和deviceId放到了Cookie中。每次請求的終端都需要提交終端Token和繫結用的場景資訊(deviceId),因為驗證的時候資料庫Token儲存的是由它們雜湊過來的值,因此驗證的時候也是使用一樣的構建過程(即Demo裡面的BuildDatabaseToken方法),這樣終端Token和資料庫Token就有了對應關係。得到資料庫Token就能在資料庫裡面查詢了(即上圖的loginTokenRepository.FindUser方法)。Demo的驗證頁面是Home/Index,裡面使用了過濾器CheckLoginTokenActionFilterAttribute做驗證,在需要驗證的Controller或Action上做ServiceFilter屬性標記處理即可。

        這裡有幾點要注意的:

  • 如果使用Http做介面且有App接入,不方便地支援Cookie機制的話可以改為放在請求頭中
  • 如果使用Http且為Web瀏覽器,終端Token儲存的Cookie應該設為HttpOnly,讓JS不可觸碰

        到這裡童鞋們知道為什麼Token拆成兩部分了嗎?整個Token授權過程是單向不可逆的,而且每個使用者都有自己的雜湊鹽來生成Token,這樣能避免雜湊值被批量暴力破解,即使終端Token和資料庫Token都洩露了你也對應不上。試想一下如果不是這樣而是終端資料庫的Token是相同的,那一旦資料庫洩露那麼黑客就能模擬Token進行登入/授權了。另外資料庫Token雜湊過後長度變短,查詢效能也能提高,畢竟每個請求都需要進行驗證,查詢頻率是很高的。

 

 

五、Token失效與登入唯一性

        不論是終端Token還是服務端Token都要有失效機制,時間越短越安全,但也要結合使用場景需求來設定時長。終端Token如果是Cookie的話直接用Cookie的過期時間即可,並且要和資料庫Token的過期時間一致。資料庫Token生成的時候也要指定過期時間,Demo裡面資料庫儲存的欄位為ExpireAt。總的一共有以下幾種失效情況:

  • 到了過期時間
  • 使用者修改賬戶關鍵資訊,服務端需要主動將舊的Token全部作廢掉,如修改密碼
  • 使用者登出登入
  • 使用者使用Token重新整理機制

        另外,如果需求是一種終端只能一個登入,比如Web和App可以保持同時登入但App只能有一個登入,資料庫Token還得繫結“終端型別”,這樣在最新一次登入的時候把相同的終端型別的舊的資料庫Token全部作廢掉就好了。

        可以看出,服務端的保有的資料庫Token可以有效控制其授權,達到訪問控制的目的。

 

 

六、CAS/SSO單點登入

        CAS即中央認證服務,SSO即SSO即單點登入。很多時候這兩個會放在一起說,其實CAS是一套解決方案,SSO是一種機制描述。如果我們使用的是Http-Web那麼我們如何實現我們自己的SSO呢?很簡單,把Token和繫結的場景資訊提升到同一個域下即可。比如有總部和門店兩個系統分別使用了hq.xxxx.com/store.xxxx.com子域名,那不管從哪個系統登入,login_token和deviceId這兩個Cookie放在頂級域.xxxx.com下即可,這樣所有子系統都能訪問得到它們,繼而都保有登入/授權狀態。有沒有發現登入新浪微博後,輸入weibo.com都會先跳轉到sso然後再跳轉回來,這個也差不多,這也是為什麼你登入了新浪微博,你新浪部落格也是登入了的狀態。

 

 

七、URL授權驗證與掃碼登入

        當我們需要進行郵箱驗證的時候,有可能是使用者登入和郵箱不是一個終端的,這時候我們就需要進行URL授權驗證來避免使用者再次進行登入。其原理很簡單,在使用者點選驗證的連結上面附上URL授權令牌即可(下面簡稱URL Token),這個URL Token與登入Token不應該有關係所以應當單獨儲存。生成一個URL Token,服務端再對應儲存類似的服務端Token,這樣就有了【URL Token】 - 【服務端Token】 - 【使用者】這樣的對應關係。當使用者在有效期內點選後,服務端獲得URL Token也就能進行授權或驗證。

        掃碼登入的場景複雜一些,終端生成的二維碼其實就是一個Token(我們稱之為QR Token)這個Token是和終端繫結的。使用者拿App掃了QR碼,其實就是在App內同時提交QR Token和使用者資訊,使用者確認可以登入後服務端會頒發登入Token給終端,這樣終端就是登入狀態了,這一步也就是上面構建和驗證登入Token的過程。實際掃碼登入需要實現即時通訊,這樣終端才能做出相應的反應。另外QR Token也是一樣有過期時間的,因此那些掃碼登入的頁面會做二維碼自動重新整理的。

 

 

八、Session實現

        其實有些童鞋會納悶,完善的框架都會提供Session操作,其原理是一樣的,那為什麼我們還這麼“造作”呢?原因有二,框架自帶的可能過重,比如我就很不喜歡asp.net自帶的授權認證機制,微軟弄得一套一套的,簡直就是全家桶,笨重,自己實現一個能定製化且輕量。第二,考慮類似上面的功能實現,自己做能更靈活地實現。

        我們已經實現了登入/授權和驗證,接下來我們只要想辦法把一些資料和Token繫結在一起,並放在快取中,這些資料就是Session了。我一般的做法是封裝一個SessionService,然後定義一套Session介面。一個Session資料由TokenKey-Value組成,如果Token失效,則清理所有對應的TokenKey資料即可。就是這麼簡單粗暴,不同的快取元件實現不盡相同。

 

 

九、關於Token重新整理

        OAuth 2.0裡面有提供Token重新整理服務,即終端持有的Token快過期的時候,終端可以再呼叫重新整理介面來替換快過期的Token,達到永續狀態。簡單來說就是請求新的Token,請求時舊Token作廢掉,實現並不複雜,參見:Oauth2.0(三):Access Token 與 Refresh Token

 

 

十、防重放攻擊與簽名機制

        重放攻擊(Replay Attacks)又叫重播攻擊,防範這個其實和本文討論的主題沒關係。完整實現的介面都有實現,欲知詳情,等我下一篇。

 

        花了好幾天來寫了這篇文章,同時也是自己對這一技術點的總結歸納,有不對的地方還請指正。

 

相關連結:

ASP.NET Web API與Owin OAuth:呼叫與使用者相關的Web API

微信公眾平臺技術文件 - 獲取access_token

相關文章