asp.net 身分驗證類型與基本原理(一)
這一篇很特別,我們要來談談 asp.net (.net core) 的身分驗證類型與基本原理(架構)。
最近這幾年,你可以從網站上找到任何你想找的資訊,特別是程式碼片段,但我總是眼睜睜的看著學員或客戶,從莫名的網路上抄來一份代碼,試著崁入自己的程式,無奈就是怎麼塞都塞不進去,不然就是硬塞進去之後,運行起來不如預期。
原因很簡單,因為硬Copy過來別人寫的東西,我們往往可能根本不知道那段程式碼寫的是什麼,只能硬插,無從改起。
這種狀況這幾年看了不少,身分驗證的部分更加明顯,有同學在寫WebAPI,但卻想用登入畫面去驗證呼叫者身分(!?),有人明明在寫MVC,卻一直在研究怎麼用JWT Token登入…細問同學為什麼要這麼做? 答案總是很有意思。
所以,一直想寫篇文章,來釐清一下這些東西。
asp.net 網站的身分驗證(保存)類型
asp.net 不管是哪一個版本,不管是 .net framework 或是 .net core,本質上都是一樣的。大部分情境下(這世界總有特例,文章也總是有背景,我們討論的是『大部分』情境下,若有特例,請讀者自行轉換),伺服器端保留(維持、識別) 用戶身分的機制大概有底下幾種:
- asp.net的Session變數
- aps.net Cookies 驗證
- Token (最近好像很流行 JWT)
首先,你必須知道一個前提,所有的Web應用程式和網路上伺服器和用戶端(瀏覽器)之間的交互(資料傳遞、呼叫)行為,本質上都是無狀態的(stateless),也就是說,你不能假設伺服器端知道用戶是誰,或是伺服器端連續兩次的呼叫是不是由同一位用戶觸發的。
這點跟你的經驗可能不符,你會覺得,網站(伺服器端)知道你是誰啊?不然用戶幹嘛登入? 登入後伺服器端不就知道用戶是誰了嗎?
對,許多的網站都能夠登入,如果是無狀態,那登入怎麼實現的?
好,實現的方式大多是底下這樣,在伺服器端與用戶端溝通(互動)的過程中,會在 http message(大多是header)中,夾帶著一個記號,這個記號具有唯一性,每一個用戶都不同,這個記號隨著網頁每次的來回傳遞(或是WebAPI的被呼叫),被夾帶在訊息(http message)之間,因此可以讓伺服器端知道當前的用戶端是哪一位。(這個記號大多只是一組ID,用來讓伺服器端識別用戶端,而非在記號中直接存放用戶名稱)
不管你用前述三種的哪一種身分保存方式,本質上最終都有這樣的一個機制存在。而預設狀況下,要實現這樣的結果,最方便的實作就是Cookies。
所以,即便你用伺服器端Session保存資訊,預設狀況下,也會用到Cookies,作為伺服器端與用戶端之間的關聯(associate),否則伺服器端根本不知道這Session是屬於哪一個用戶的。
參考: https://stackoverflow.com/questions/2589823/do-session-use-cookies
使用Session保留用戶身分
好,那我們先來談Session。
就如同我們大家所知,asp.net 的Session變數是存放在伺服器端,可以在單一Domain & 單一用戶的狀況下,進行跨頁面存取。(Session可以跨頁面、但不能跨網站、不能跨用戶)
所以,古時候很多網站的身分保存機制,是製作一個有帳密輸入功能的登入網頁,當用戶輸入帳密後按下登入,成功驗證帳號密碼與資料庫中保存的相符之後,就把用戶的帳號(或email)存在某個Session變數中,然後就視為該用戶已經登入完畢,且由於該Session變數可以跨網頁存取,因此在頁面巡覽(切換,跳頁)的過程中,只要該Session沒有過期(Timeout),後端都可以將前端該用戶視為已登入,並取得該用戶的身分(email或帳號)。
在這種模式底下,檢查用戶身分的方法,就是在網頁的後端程式碼中檢查保存用戶身分的Session變數內的值,如果有值,則表示已登入,並可取得用戶身分。
但別忘了,這狀態可以work,骨子裡還是因為用到了Cookies。
最近我們不建議用Session作為身分驗證方式,因為難以實現附載平衡或是高可用性,Session實質上是儲存在伺服器端的記憶體中的,當伺服器端有多台伺服器時,不容易跨伺服器保存。雖然可以採用資料庫作為Session的儲存位置,並實現跨伺服器(Web Server)使用Session,但如此一來用Session的意義就不大了。(因為犧牲了速度,最終跟使用Cookies可能差不了多少)
使用Cookies驗證
asp.net 本來就有一個 Cookies 驗證(cookies authentication)機制,這個機制本質上背後的作法跟前面我們一開始說的模式也差不多,就是在伺服器端(Server)與用戶端(Browser)溝通的過程中,保留一個具有唯一性(unique)的 State Cookie,並且把這個狀態Cookie在每一次瀏覽器的呼叫(post/get/…)過程中,傳遞給伺服器端。這樣伺服器端就可以知道用戶是同一個,然後伺服器端把資料或HTML回傳給用戶端的時候,也把Cookie回傳給用戶端。所以,你會看到,asp.net的http訊息當中,常常會看到底下這段:
因此,你會看到實作了 cookies 驗證的 asp.net core 網站,在http訊息中會夾帶著上面這樣的『記號』。透過這個唯一值,伺服器端就可以得知用戶端的身分了。而這一整段是由 asp.net 框架自動完成,所以開發人員大多不用寫什麼Code,可以直接用 user.identity.name 取得登入者是誰,用戶的登入與登出也大多只需要一兩行程式碼就可以搞定,使用起來非常方便。
有興趣可以參考筆者的 github 上 .net core 的範例:
https://github.com/isdaviddong/dotnetCore-PureWebAppLogin
文章:
https://studyhost.blogspot.com/2021/03/cookieswebapi_1.html
安不安全?
到這邊我們先岔題一下,待會再來談 Token。
先說,Session有沒有比較安全? 因為很多人一開始以為 Session是完全儲存在伺服器端的,所以一定比較安全! 但事實是,Session 必須倚賴傳遞於用戶端與伺服器端之間的 Cookies,因此結果最後跟使用Cookies驗證的安全性大致差不了太多。
再來,Cookies是否容易被惡意複製或竊取? 答案是,不無可能,但有難度。
我們擔心 Cookies 被複製的主要原因,是擔心用戶身分被他人偽造(冒用)。要討論這個,重點在,這個他人是誰? 他在哪裡? (為何這重要? 待會你就知道)
主要有兩種可能:
1.在網際網路(或區域網路)上
2.在你的電腦上
先討論在網際網路上,有沒有人能夠在網路上偷看或竊取你的Cookies 資料? 這部分有個關鍵,就是傳遞的過程中有沒有走 SSL 加密,也就是 https 的protocal,如果沒有,那你的傳遞資料基本上是透明的,網路上人人可看,也就沒有安全性可言。如果你的傳輸過程是走 https ,那『理論上』,只有『瀏覽器』和『伺服器』兩端可以『看懂』你的資料,路上的其它人(駭客、設備、路由器、網路線、分享器…)看到了也看不懂,不容易竄改或偽造。
再講另一種Cookies被複製或竊取、偽造的可能位置,就是『在你的電腦上』。倘若,Cookies最終被儲存到電腦上的某個位置(硬碟),那當然還是有被複製(或竊取)的可能。只是,這個可能性有多高? 而且哪邊是可能的漏洞?
要知到,所有的資訊都可能存在於幾個位置,像是記憶體、畫面、硬碟…記憶體內的資料相對難被竊取(因為大部分的應用程式有獨立的行程,不能共享記憶體)、螢幕上的東西只要拿手機拍照就好(沒事不要把機密資料攤在螢幕上)、硬碟內的東西也相對容易被偷(應用程式有機會可以讀取到別的應用程式儲存的檔案或資料),所以有些公司崇尚『資訊不落地』,敏感的東西不得隨意儲存,只能在記憶體內使用。所以Chrome瀏覽器的無痕模式,會在關閉後清除所有Cookies。
但其實,上述Cookies最有可能的漏洞的確是『瀏覽器』,因為網路傳輸的過程中,只有瀏覽器碰到該資料(其它過程都有SSL保護),使用無痕瀏覽器時,關閉了視窗Cookies也被清除,但倘若瀏覽器本身有問題(來路不明、或被瀏覽器外掛給駭了),那當然就沒啥安全性可言。
所以,亂裝瀏覽器外掛其實很危險。另外,瀏覽器是透過OS(作業系統)運行,如果作業系統本身被駭了,那當然也有極高的安全性疑慮,因為控制了作業系統就控制了一切,隨時可以偷看你的記憶體或硬碟。所以,所有的作業系統安全性更新,都應該第一時間安裝。
所以結論是,如果走 https 的網站,在使用安全無虞的瀏覽器,且作業系統也沒安全性疑慮的狀況下,cookies應當是安全且不易被『非法』複製的。
框一個非法,因為用戶還是可以合法地去檢視和複製cookies。
總的來說,如果你的瀏覽器有問題,或是作業系統有問題。那沒什麼好說的,任何上網或其他運作都有風險疑慮。所以資安的第一個前提,就是不能讓駭客以任何方式接觸到實體設備,如果不能確保這個,那沒任何資安可言。
但如果瀏覽器可以被信任(不是雜牌瀏覽器),又都走https通訊,用戶也謹慎的使用無痕瀏覽模式,那安全性算是相當高的。
公用電腦上使用瀏覽器,一定要使用無痕視窗
什麼是 token
接著我們談 token,你常常聽到的 JWT就是token的一種。token,翻成中文比較好的翻譯是『令牌』,古時候武裝劇裡面的『見令如見人』的那種令牌。
拿著令牌,多半意味著可以存取些什麼資源。因此,大多數Token的運作行為(發放和使用)類似底下這樣。
- 由管理資源的單位(網站或系統, 例如管Google Drive的Google)發一個Token
- 這個Token代表著某個用戶授予的某種授權(例如David授予某人或某系統存取David的Google Drive中的文件的權限)
- 因此發token時,Google必須經過該用戶確認(所以David必須輸入Google帳密,當然是在Google的網站上,帳密也無須給他人知道,一般這類Token中也不會包含帳密或email)
- token發出後,可以交給某個系統(或某人)
- 得到該Token的人(或系統),就可以拿著這個token,向管理資源的單位(例如Google)取得用戶授予的某些資料(就是有存取該用戶 Google Docs的權限)
Token最典型的用法是上面這樣。反正就是個『令牌』的概念。
第三方網站(或是App),在需要存取用戶的Google Drive時,就是依照上面的流程,引導用戶去Google網站完成驗證(輸入帳密,同意授予權限),待取得Google發送的Token(令牌),第三方網站(或App)拿著這個令牌,就有權限去Google網站(一般是透過呼叫API的方式)以用戶授予的權限做些(或取得些)什麼。整個流程就是這樣。
Token的使用場合?
因此,token大多是用來證明自己(應用程式、呼叫端、第三方網站)具有存取某些資源的權限。你常會看到,用戶使用某個App的時候,應用程式讓你連結到Google網站(大多走OAuth模式),讓你同意授予某些權限,App的這個動作就是在取得你授予的Token,取得後,應用程式會把該Token保留起來,以便於後續使用。
你可能會擔心,那該應用程式或第三方網站不就可以存取你的某些資源(例如Google Docs, Calendar…etc.)? 是的,正是如此,因此Token多半有時效性,可能是數天,也可能是一兩年,依照授權決定。那萬一Token外洩,撿到的人不就有可能用該App或第三方網站的身分來存取你的資源? 也沒錯,所以Token多半不會公開顯示,傳輸過程也必須透過加密(SSL),甚至儲存位置也要謹慎(最好別儲存)。
Token安不安全?
安不安全,就看你(第三方網站或App)怎麼使用!
Token的本質跟secret差不多,基本上都算是機密資料,不該公開,不能外洩,不應隨意儲存。最安全的使用方式是取得Token立即拿來用,用完就丟(刪除),不要儲存在任何地方,只停留在記憶體內,這樣當然最安全。
但如果有一個應用程式(或網站),一天到晚需要你重新登入Google帳密只為了取得新的Token,你一定會覺得很煩,所以大多應用程式取得Token之後,都會自行保存起來,能用多久就用多久(這意味著,只要Token沒失效,該應用程式就可以存取你的資源。你發了這Token,其實就意味著你信任該應用程式)。而該應用程式把Token存在哪裡,就成了關鍵。
理想的存放位置是後端DB或是更安全的環境,手機App取得Token之後,如果存放在App的storage是否安全? 網站取得Token後,如果放在 local storage是否安全? 這都是相對的,只要儲存了,就『相對』不安全。但如果不儲存(或是效期太短),需要使用時就得常常重新取得Token,一天到晚讓用戶重複輸入帳密,不僅麻煩,也不見得安全。
所以儲存是迫不得已的事,而儲存安不安全,跟前面的Cookies相同,只要OS和App本身不被駭,不會有其他程式碼有機會讀取到該App的資料,那基本上是安全的。
Token的用途
所以,Token主要的(原始的)用途就在授予某種權限,應用程式可以保存Token,無須知道用戶帳密,就可以拿著Token存取用戶授予的資源。這也是Google Docs、Calendar這類的資源常常可以安全的讓第三方應用程式或網站存取的原因。
除此之外,Token也可以被用來作為具有某種身分的象徵。例如前後端分離的SPA網站,當用戶驗證完帳號密碼之後,後端可以自己發一個Token給前端,前端取得該Token之後,就可以在未來呼叫後端的API時,把Token當作參數在http header中,傳遞給伺服器端,伺服器端的WebAPI可以檢視該Token是否有效是否到期,來決定是否允許前端呼叫。
而這種身分驗證方式採用的Token格式,近年來最常見的做法就是 JWT(JSON Web Token)。JWT是一種open source的Token格式,任何開發人員都可以自行依照該格式建立出一個Token,其格式包含三個部分,分別是:
- Header
- Payload
- Signature
我們暫且不討論它的技術細節,你可以在底下這個連結看到更多詳細的內容: https://en.wikipedia.org/wiki/JSON_Web_Token
如果你有興趣,可以用底下這個網站自己建立一個 JWT:
http://jwtbuilder.jamiekurtz.com/
然後用底下這個網站來解析(Decoded) JWT:
https://jwt.io/
你會發現,JTW中的Payload,可以包含登入者的身分資訊(或其他必要資料,因為不需要secret即可decoded,所以很方便作為用戶身分的保存與取得),且因為Signature有透過secret做Hash,則可用來讓token的發行者作為檢核之用,確保該token是自己發行的,不曾被竄改。
對於asp.net應用程式而言,一般來說,JWT有底下幾種典型用途:
- 前後端分離的SPA(Single Page Application)應用程式的身分保存(也就是無狀態下的身分維持機制)
- WebAPI呼叫時的身分驗證
上面這兩種作法的行為,其實都很類似前面一開始介紹的模式,用戶從前端輸入帳密,呼叫後端API驗證完身分之後,後端自行建立一個JWT(Token)發給前端。未來,前端(Browser上的JavaScript)有任何對後端(WebAPI)的呼叫,都使用這個Token作為身分識別(作法是夾帶在http header中)。
後端WebAPI取得這個Token之後,可以驗證該Token是否為自己所發(透過Signature),也可以得知用戶的身分(透過Payload)。如此一來,簡單的解決了SPA和WebAPI呼叫的身分驗證(身分抓取)問題。
這和Cookies驗證有何不同?
講到這邊,我真的覺得我寫太多了,一寫起來,文章像是自己有生命一樣停不來。不過,有了前面的基礎,我們開始可以討論一些問題了…
不知道你會不會聯想到一個問題,這樣JWT Token若在SPA網站上使用,跟前面的Cookies驗證有何不同?
你會發現。其實本質上是差不多的。身分驗證完畢之後,給前端發個記號(令牌),接著在前後端交互(WebAPI呼叫、或是頁面巡覽)的過程中,在http header中夾帶著這個記號(令牌),讓伺服器端可以驗證,並且達到識別且維持身分的效果。差別只在 Cookies驗證中採用的是Cookies,而JWT則是自己發的Token。
所以,傳統的非SPA網站,例如 asp.net MVC(含WebForm)或是 .net core的MVC或Razor Page,是否需要以JWT做身分識別? 從我的角度來說,我自己覺得其實不需要,採用內建的 Cookie Authentication就可以了(輕鬆方便)。若採用JWT反而麻煩,不僅發token和解析token與維持身分的code都要自己撰寫,頁面切換(submit)的過程中,要保存token不落地(不儲存)也頗煩人,最後還是得把JWT(Token)存在localstorage或cookies中,結果跟使用cookie authentication本質上沒啥兩樣。
SPA的身分驗證機制
但SPA(Single-Page Application)呢? SPA則毫無疑義,使用JWT方便得多,而且由於SPA大多徹底的實踐了前後端分離,因此換頁(submit)的機會少,大多前後端交互都採用AJAX呼叫,所以透過JWT保存(維持)身分、呼叫後端API都很方便。因為前端不太需要換頁,所以JWT可以安全的保存在記憶體中就好,無須寫入Local Storage,瀏覽器關掉就失效,下次開啟瀏覽器重新登入即可,這樣才能有高安全性,否則隨意儲存JWT(Token)其實風險可能更高。
WebAPI的身分驗證
那WebAPI的身分驗證呢?
我自己覺得使用Cookies驗證或JWT(Token)的形式其實都可以,因為兩種都能輕易達成。如果我的網站本質上是MVC或RazorPage,那我會選用 Cookies驗證。但如果我的網站是SPA,那我會選擇JWT,總之跟著網站(網頁)的行為就好,簡單方便又具有一致性,無需特別做另一套。但如果你沒有具有網頁的網站,只是單純做WebAPI(給前端或其他應用程式呼叫),則隨意,用哪一種都可以。但如果用戶端只有手機,或是用戶端有可能跨網域,那我會選擇JWT,因為手機或Web/Desktop應用程式要模擬Cookies的保存必須實作一個 Cookies Container,相對麻煩一些。
總結
到這邊,你大概知道了 ASP.NET Web應用程式(不管是 .net core或是 .net framework,不管是MVC或Razor Page或SPA)進行身分保存(維持)與實現登入功能的原理。我們後面接著要來討論,在這個原理底下,Web應用程式的SSO(Single-Sign On)是如何實現的?
未完,待續
留言