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


        本篇文章將從無到完整的登錄框架或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)是可有可無的,可以根據實際業務來處理,有必要的話再增加其他相關聯的數據和表。C/S或App的話DeviceId可以用系統的標識Id,像Web瀏覽器的話因為拿不到類似的東西,我們可以指定一個在Cookie里頭,Demo就是這么干的。

 

 

三、Demo源碼介紹

用戶Controller 

HomeController

UserController - 用戶注冊、登錄、注銷登錄

HomeController - Index - 默認啟動頁,Token驗證頁

 

 

四、構建與驗證Token

Token構建

構建Token

驗證Token

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

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

授權Token構建關系圖

授權Token構建關系圖

 

        這里有幾點要注意的:

  • 終端Token應該有足夠的長度,且每次應隨機生成,因此才有Guid.NewGuid()參與求值
  • 終端Token參與生成的userId、name是起到了鹽作用,讓整個構建更加復雜(經提醒已經排除掉了密碼的參與,哈希雖然很難破解但還是謹慎點好)
  • 不論是終端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有效即可。

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

 

 

六、CAS/SSO單點登錄

        CAS即中央認證服務,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM