網(wǎng)站建設(shè)價(jià)格差異多少百度推廣找誰(shuí)
在之前的《通過(guò)實(shí)例理解Go Web身份認(rèn)證的幾種方式[1]》和《通過(guò)實(shí)例理解Web應(yīng)用授權(quán)的幾種方式[2]》兩篇文章中,我們對(duì)Web應(yīng)用身份認(rèn)證(AuthN)和授權(quán)(AuthZ)的幾種方式做了介紹并配以實(shí)例增強(qiáng)理解。
在現(xiàn)實(shí)世界中,還有一大類(lèi)的認(rèn)證與授權(quán)是在前面的文章中沒(méi)有作為重點(diǎn)介紹的,那就是OAuth2授權(quán)[3]與基于OAuth2之上的OpenID身份認(rèn)證(OIDC, OpenID Connect)[4]。
近期接觸到開(kāi)放平臺(tái)(Open Platform)的設(shè)計(jì)和開(kāi)發(fā),整個(gè)開(kāi)放平臺(tái)的授權(quán)流程都是基于OAuth2打造的,因此在這篇文章中,我就來(lái)先來(lái)通過(guò)實(shí)例詳細(xì)說(shuō)說(shuō)OAuth2,OIDC將放在后面的文章中說(shuō)明。
1. OAuth是什么
OAuth中的O代表了Open,OAuth直譯過(guò)來(lái)就是開(kāi)放授權(quán)。OAuth是一個(gè)開(kāi)放標(biāo)準(zhǔn)[5],允許用戶(hù)讓第三方應(yīng)用訪(fǎng)問(wèn)該用戶(hù)在某一網(wǎng)站上存儲(chǔ)的私密的資源(如照片,視頻,聯(lián)系人列表等),而無(wú)需將用戶(hù)名和密碼提供給第三方應(yīng)用。
OAuth不是什么新技術(shù)了,其原型最早可追溯至2006年末,至今也快小20年了。但直到2010年4月,OAuth 1.0協(xié)議才以RFC 5849的形式正式發(fā)布[6]。不過(guò)OAuth 1.0版本存在兩個(gè)主要問(wèn)題,一是當(dāng)初設(shè)計(jì)時(shí)僅是為了web瀏覽器應(yīng)用,隨著應(yīng)用類(lèi)型的增多,一套授權(quán)機(jī)制難以應(yīng)對(duì)現(xiàn)實(shí)中的所有場(chǎng)景,比如:Web應(yīng)用場(chǎng)景、移動(dòng)App應(yīng)用場(chǎng)景、官方應(yīng)用場(chǎng)景等,因?yàn)檫@些場(chǎng)景不是完全相同的,這給使用者帶去很多負(fù)面體驗(yàn);二是一些安全性的問(wèn)題(這里就不展開(kāi)了)。
2012年10月,解決OAuth 1.0上述問(wèn)題的OAuth 2.0以RFC 6749發(fā)布[7]。OAuth 2.0是OAuth協(xié)議的下一版本,但不向后兼容OAuth 1.0。OAuth 2.0關(guān)注客戶(hù)端開(kāi)發(fā)者的簡(jiǎn)易性,同時(shí)為Web應(yīng)用、桌面應(yīng)用、手機(jī)和智能設(shè)備定義了專(zhuān)門(mén)的授權(quán)許可流程,包括:授權(quán)碼許可機(jī)制(Authorization Code)、客戶(hù)端憑據(jù)機(jī)制(Client Credentials)、資源擁有者憑據(jù)機(jī)制(Resource Owner Password Credentials)和隱式許可機(jī)制(Implicit)。如今OAuth 1.0已經(jīng)被廢棄,談到OAuth如無(wú)特殊說(shuō)明,指的都是OAuth2.0版本。
在OAuth2.0的四種授權(quán)類(lèi)型中,最安全的、最廣泛使用的也是最常見(jiàn)的就是授權(quán)碼許可機(jī)制(Authorization Code)了。本文后續(xù)的說(shuō)明與示例也都是圍繞授權(quán)碼這種類(lèi)型的。
2. OAuth2解決了什么問(wèn)題
僅僅憑借上面關(guān)于OAuth的描述,你可能依然無(wú)法對(duì)OAuth有一個(gè)直觀和深刻的理解,筆者第一次接觸OAuth協(xié)議時(shí)也是花了不少時(shí)間才逐漸“茅塞頓開(kāi)”,當(dāng)然本文參考資料中的那些書(shū)籍和資料“功不可沒(méi)”,尤其是OAuth 2.0的RFC協(xié)議規(guī)范[8]。
那么OAuth到底解決的是什么問(wèn)題?這里我們就用一個(gè)非常典型的示例來(lái)系統(tǒng)說(shuō)說(shuō)一下。
2.1 傳統(tǒng)的云盤(pán)系統(tǒng)
現(xiàn)在有一個(gè)像百度網(wǎng)盤(pán)那樣的云盤(pán)系統(tǒng)(my-yunpan.com),用戶(hù)可以注冊(cè)云盤(pán)系統(tǒng)的賬號(hào),然后將自己的個(gè)人數(shù)據(jù)文件,比如照片、音視頻、文檔等上傳到云盤(pán)上保存。
假設(shè)這里有一個(gè)名為tonybai的用戶(hù)注冊(cè)了云盤(pán),并將個(gè)人的一些照片文件上傳到云盤(pán)上做保存和備份:
這是一個(gè)我們都可以理解的場(chǎng)景,用戶(hù)注冊(cè)云盤(pán)賬號(hào),然后登錄云盤(pán)應(yīng)用后將個(gè)人照片上傳到云盤(pán),這里使用的身份認(rèn)證和授權(quán)技術(shù)方案沒(méi)有超出《通過(guò)實(shí)例理解Go Web身份認(rèn)證的幾種方式[9]》和《通過(guò)實(shí)例理解Web應(yīng)用授權(quán)的幾種方式[10]》兩篇文章的范疇。
針對(duì)這個(gè)場(chǎng)景,OAuth定義了三個(gè)很容易理解的概念實(shí)體:
Resource Server:集中存儲(chǔ)資源(如用戶(hù)照片等)的服務(wù),這個(gè)示例里就是云盤(pán)服務(wù);
Resource owner:資源的擁有者,這里就是云盤(pán)的用戶(hù),比如圖中的tonybai;
Protected Resource:Resource owner上傳并存儲(chǔ)在Resource Server中的Resource,受Resource Server保護(hù),這里對(duì)應(yīng)的就是用戶(hù)上傳的照片。
大家先對(duì)這三個(gè)概念實(shí)體有個(gè)感性的認(rèn)識(shí)即可,后續(xù)備用。
2.2 第三方的照片沖印服務(wù)
智能手機(jī)時(shí)代,數(shù)字照片(Digital Photo)將傳統(tǒng)的基于膠卷的照片徹底拉下神壇。數(shù)字照片是存儲(chǔ)在磁盤(pán)、手機(jī)中或云盤(pán)上的,但依然有很多人有將數(shù)字照片像傳統(tǒng)照片那樣沖印出來(lái)放在相冊(cè)里或房間照片墻上欣賞的需求,于是就有了在線(xiàn)照片沖印服務(wù)(my-photo-print.com)。
用戶(hù)注冊(cè)在線(xiàn)照片沖印服務(wù)后,將自己的數(shù)字照片上傳,交錢(qián)沖印即可,沖印好的照片便會(huì)經(jīng)由快遞送至用戶(hù)家中,非常方便。
2.3 Resource Owner要沖印照片
有一天,云盤(pán)用戶(hù)tonybai要挑選一些近期存儲(chǔ)在云盤(pán)中的照片進(jìn)行沖印,他搜索到了my-photo-print.com這個(gè)第三方的照片沖印服務(wù),但他需要在my-photo-print.com這個(gè)應(yīng)用上重新注冊(cè)一個(gè)賬號(hào),再將云盤(pán)上的照片下載后重新上傳到my-photo-print.com這個(gè)服務(wù)的空間中才能實(shí)現(xiàn)在線(xiàn)沖印。這對(duì)于大多數(shù)像tonybai這樣的用戶(hù)而言并不是一個(gè)很easy的操作,體驗(yàn)上也是糟糕。tonybai在思考:我的照片已經(jīng)在云盤(pán)上了,為什么不可以直接基于云盤(pán)上的照片進(jìn)行沖印呢?
2.4 增加開(kāi)放平臺(tái)(open.my-yunpan.com)
在tonybai萌生出這個(gè)困惑的同時(shí),云盤(pán)的產(chǎn)品經(jīng)理也同步感知到了這個(gè)需求,是時(shí)候給云盤(pán)系統(tǒng)增加開(kāi)放平臺(tái)了!這樣,第三方應(yīng)用便可以接入云盤(pán)系統(tǒng),方便快捷地為云盤(pán)用戶(hù)提供各種擴(kuò)展服務(wù),比如照片沖印、云上視聽(tīng)、數(shù)據(jù)智能管理等,這也是互聯(lián)網(wǎng)界熟知的生態(tài)建設(shè)的套路。
下面是照片沖印服務(wù)my-photo-print.com注冊(cè)和接入開(kāi)放平臺(tái)的示意圖:
照片沖印服務(wù)my-photo-print.com注冊(cè)和接入云盤(pán)開(kāi)放平臺(tái)后,會(huì)得到一個(gè)client_id和client_secret,這兩個(gè)字段是照片沖印服務(wù)接入云盤(pán)開(kāi)放平臺(tái)的憑據(jù),即云盤(pán)開(kāi)放平臺(tái)對(duì)第三方應(yīng)用進(jìn)行身份認(rèn)證的憑據(jù)。
不過(guò)即使照片沖印服務(wù)使用client_id和client_secret這個(gè)憑據(jù)通過(guò)了云盤(pán)系統(tǒng)的認(rèn)證,照片沖印服務(wù)依然拿不到云盤(pán)系統(tǒng)上用戶(hù)的照片數(shù)據(jù),這里需要一個(gè)授權(quán)過(guò)程,即云盤(pán)系統(tǒng)用戶(hù)(如前文提及的tonybai)告訴云盤(pán)系統(tǒng)是否允許照片沖印服務(wù)訪(fǎng)問(wèn)自己的數(shù)據(jù)。
那么問(wèn)題來(lái)了!如何實(shí)現(xiàn)云盤(pán)系統(tǒng)用戶(hù)對(duì)已接入云盤(pán)系統(tǒng)的第三方應(yīng)用的授權(quán)呢?下面我們就來(lái)探討一下。
注:這里顯然是打了個(gè)伏筆,一旦云盤(pán)系統(tǒng)建立了開(kāi)放平臺(tái),那么云盤(pán)系統(tǒng)的用戶(hù)對(duì)第三方應(yīng)用的授權(quán)流程也就由開(kāi)放平臺(tái)規(guī)定好了。
2.5 云盤(pán)系統(tǒng)用戶(hù)對(duì)第三方應(yīng)用進(jìn)行授權(quán)的方案
2.5.1 憑據(jù)共享方案
一個(gè)最簡(jiǎn)單粗暴的方案就是直接用云盤(pán)系統(tǒng)用戶(hù)的憑據(jù)代替用戶(hù)去云盤(pán)系統(tǒng)讀取該用戶(hù)的數(shù)據(jù),下面是該方案的示意圖:
我們看到:照片打印服務(wù)想要獲取用戶(hù)的照片,它首先會(huì)提示用戶(hù)輸入其云盤(pán)系統(tǒng)上的用戶(hù)名和密碼,然后就會(huì)拿著用戶(hù)的這些憑據(jù)合法地進(jìn)入到該用戶(hù)在云盤(pán)系統(tǒng)中的個(gè)人空間并拿到想要的數(shù)據(jù)。這也意味著用戶(hù)在云盤(pán)系統(tǒng)上可以進(jìn)行的任何操作,照片打印服務(wù)也都有權(quán)限進(jìn)行。此外,一旦用戶(hù)在多個(gè)網(wǎng)站應(yīng)用上使用的是相同的用戶(hù)名和密碼,那么照片打印服務(wù)也可以通過(guò)拿到的憑據(jù)登錄這些網(wǎng)站,并“假扮”用戶(hù)獲得這些網(wǎng)站上的用戶(hù)數(shù)據(jù)。這種通過(guò)憑據(jù)共享來(lái)實(shí)現(xiàn)第三方應(yīng)用訪(fǎng)問(wèn)云盤(pán)上的用戶(hù)數(shù)據(jù)的方案顯然是毫無(wú)安全底線(xiàn)可言。
2.5.2 專(zhuān)用密碼方案
現(xiàn)在你已經(jīng)看到,共享用戶(hù)密碼并不是一個(gè)好方法,那會(huì)授予照片打印服務(wù)全局的訪(fǎng)問(wèn)權(quán)限,它就能代表由它指定的任何用戶(hù)并訪(fǎng)問(wèn)云盤(pán)系統(tǒng)上的所有照片。那是否可以授予照片打印服務(wù)一個(gè)權(quán)限有限的專(zhuān)用密碼來(lái)實(shí)現(xiàn)照片獲取呢?此密碼僅用于透露給第三方服務(wù),用戶(hù)自己并不會(huì)使用這個(gè)密碼來(lái)登錄,只是將它粘貼到所使用的第三方應(yīng)用里(如下圖):
這是一個(gè)可行的方案,但這種方案的可用性并不好。它要求用戶(hù)除了管理自己的主登錄密碼之外,還要?jiǎng)?chuàng)建(在云盤(pán)系統(tǒng)中)、分發(fā)(貼到照片打印服務(wù)系統(tǒng)中)和管理特殊的憑據(jù)。并且,用戶(hù)管理這些憑據(jù)時(shí)一般不會(huì)區(qū)分專(zhuān)用憑據(jù)與第三方應(yīng)用的對(duì)應(yīng)關(guān)系,往往是建立一個(gè)新專(zhuān)用憑據(jù)后,貼到所有第三方應(yīng)用中使用,這使得撤銷(xiāo)某個(gè)具體第三方應(yīng)用的訪(fǎng)問(wèn)權(quán)限變得很困難。讓用戶(hù)科學(xué)管理這些憑據(jù),本身就給用戶(hù)帶來(lái)了心智負(fù)擔(dān),也可理解為一種不好的體驗(yàn)。
不過(guò),相對(duì)于憑據(jù)共享方案的不安全,專(zhuān)用密碼方案已經(jīng)是有所進(jìn)步了,但還遠(yuǎn)非理想。
2.6 OAuth2授權(quán)方案
前面無(wú)論是共享憑據(jù)還是專(zhuān)用密碼方案,都繞開(kāi)了開(kāi)放平臺(tái),這顯然是故意為最終理想方案的出爐做鋪墊的 -- 沒(méi)有差方案,如何才能體現(xiàn)出理想方案的好呢!--?是時(shí)候叫出超級(jí)飛俠了!。
這就是我們提到的OAuth2授權(quán)方案。OAuth協(xié)議的設(shè)計(jì)目的是:讓用戶(hù)(Resource owner,比如tonybai)通過(guò)OAuth協(xié)議將他們?cè)谑鼙Wo(hù)資源(Protected Resource,比如照片)上的部分權(quán)限委托給第三方應(yīng)用(比如照片沖印服務(wù)),使第三方應(yīng)用能代表他們執(zhí)行操作。這個(gè)方案既要考慮提升用戶(hù)的使用體驗(yàn),也要考慮提升方案整體的安全性。為實(shí)現(xiàn)這些,OAuth在流程中引入了另外一個(gè)組件:授權(quán)服務(wù)器(Authorization Server)。
如果我們將第三方應(yīng)用(比如照片沖印服務(wù))稱(chēng)為client(客戶(hù)端應(yīng)用),加上授權(quán)服務(wù)器(Authorization Server)以及前面提到的三個(gè)概念:Resource Server、Resource owner和Protected Resource,我們就有了5個(gè)實(shí)體。他們究竟是什么關(guān)系呢,又是如何交互的呢?這就是OAuth2.0協(xié)議的核心內(nèi)容。下圖是來(lái)自O(shè)Auth2.0 RFC中的抽象協(xié)議流程圖,為了好理解,我在圖中加入了各個(gè)實(shí)體對(duì)應(yīng)的示例中的名字:
這是一個(gè)抽象圖,我們無(wú)法從中看出各個(gè)流程的細(xì)節(jié),但大致可以看出OAuth2授權(quán)的關(guān)鍵環(huán)節(jié):
client(客戶(hù)端應(yīng)用,如照片沖印服務(wù))需要用戶(hù)(Resource owner)的授權(quán),但這個(gè)授權(quán)過(guò)程,用戶(hù)不會(huì)將密碼等憑據(jù)暴露給client;
client憑借授權(quán)信息到授權(quán)服務(wù)器(Authorization server)換取access token;
client憑借access token訪(fǎng)問(wèn)用戶(hù)(Resource owner)在Resource Server(比如云盤(pán)系統(tǒng))上的Resource數(shù)據(jù)(比如照片)。
接下來(lái),我們來(lái)看看細(xì)節(jié),我們使用OAuth2中最廣泛使用的授權(quán)碼方案(Authorization code)來(lái)展示這個(gè)流程,下面是來(lái)自O(shè)Auth2.0 RFC中的授權(quán)碼方案流程圖:
這個(gè)流程圖依然很抽象,我們用下面的“分解動(dòng)作”來(lái)解釋。
2.6.1 用戶(hù)(Resource Owner)通過(guò)瀏覽器訪(fǎng)問(wèn)第三方應(yīng)用(Client,my-photo-print.com)
用戶(hù)要想使用第三方應(yīng)用,比如my-photo-print.com服務(wù)來(lái)沖印自己位于云盤(pán)上的照片,他首先要訪(fǎng)問(wèn)到這個(gè)第三方應(yīng)用,如下圖所示:
用戶(hù)通過(guò)瀏覽器(User Agent)打開(kāi)my-photo-print.com服務(wù)的登錄頁(yè)面,這個(gè)頁(yè)面除了提供使用用戶(hù)名/密碼登錄之外,還提供了“使用云盤(pán)賬號(hào)”的按鈕。該用戶(hù)不想重新注冊(cè)一遍my-photo-print.com服務(wù)的賬號(hào),選擇了點(diǎn)擊“使用云盤(pán)賬號(hào)”按鈕。
2.6.2 用戶(hù)(Resource Owner)被引導(dǎo)到云盤(pán)開(kāi)放平臺(tái)登錄并對(duì)第三方應(yīng)用進(jìn)行授權(quán)
當(dāng)用戶(hù)點(diǎn)擊“使用云盤(pán)賬號(hào)”按鈕后,對(duì)第三方應(yīng)用進(jìn)行授權(quán)過(guò)程便正式開(kāi)始,下面是一個(gè)示意圖:
OAuth2.0的授權(quán)碼模式的第一步便是第三方應(yīng)用(Client)需要將用戶(hù)(Resource Owner)引導(dǎo)到云盤(pán)開(kāi)放平臺(tái)(Authorization Server)的登錄頁(yè)面(/oauth/portal),為用戶(hù)授權(quán)做好準(zhǔn)備。在圖中第三方應(yīng)用my-photo-print.com通過(guò)網(wǎng)頁(yè)html內(nèi)重定向讓用戶(hù)的瀏覽器(User Agent)重定向到云盤(pán)開(kāi)放平臺(tái)的授權(quán)門(mén)戶(hù)頁(yè)面,在重定向的請(qǐng)求中,Client帶上了自己的一些參數(shù)(比如client_id、scope等)。
云盤(pán)開(kāi)放平臺(tái)(Authorization Server)返回一個(gè)用戶(hù)登錄頁(yè)面,用戶(hù)(tonybai)輸入用戶(hù)名密碼以供Authorization Server做身份認(rèn)證。注意這個(gè)過(guò)程完全沒(méi)有client(照片沖印服務(wù))的參與,用戶(hù)名和密碼不會(huì)泄露給第三方。
當(dāng)用戶(hù)(如tonybai)點(diǎn)擊submit提交憑據(jù)信息時(shí),可以向服務(wù)端請(qǐng)求,也可以像圖中簡(jiǎn)化版那樣直接給出授權(quán)范圍的提示。彈出的框提示“照片沖印服務(wù)需要用戶(hù)授予兩個(gè)權(quán)限”,如果用戶(hù)點(diǎn)擊“授權(quán)”,則會(huì)向Authorization Server發(fā)起授權(quán)請(qǐng)求,連同用戶(hù)的登錄憑據(jù)一起,授權(quán)請(qǐng)求的路徑與參數(shù)如下(也可以使用表單提交的方式提交授權(quán)請(qǐng)求):
/oauth/authorize?response_type=code&client_id=my-photo-print&state=xyz123&scope=user_info,read_photos&redirect_uri=http%3A%2F%2Fmy-phone-print.com%3A8080%2Foauth%2Fcb
response_type=code表示用戶(hù)向授權(quán)平臺(tái)請(qǐng)求一個(gè)授權(quán)碼,再?gòu)?qiáng)調(diào)一下:這個(gè)授權(quán)碼是用戶(hù)(如tonybai)去申請(qǐng)的,而不是client(第三方應(yīng)用),后續(xù)也是由用戶(hù)將code告知client(第三方應(yīng)用)。
cilent_id表示為哪個(gè)第三方應(yīng)用申請(qǐng)的,后續(xù)授權(quán)平臺(tái)在發(fā)access_token時(shí),可以基于該client_id進(jìn)行校驗(yàn)。
scope是此次授權(quán)的權(quán)限列表。
redirect_uri是一個(gè)重定向地址,這個(gè)地址可以在請(qǐng)求中傳遞,如果不傳遞,也可以在client注冊(cè)開(kāi)放平臺(tái)賬號(hào)時(shí),提供給開(kāi)放平臺(tái)(Authorization Server)。
state是一個(gè)隨機(jī)數(shù),OAuth 2.0官方建議使用state以避免CSRF攻擊。
2.6.3 用戶(hù)提供code,client用code換取access_token并讀取用戶(hù)數(shù)據(jù)
如果Authorization Server通過(guò)了用戶(hù)的請(qǐng)求,便會(huì)在應(yīng)答中帶上這次分配給用戶(hù)的授權(quán)碼(code),這個(gè)授權(quán)碼是一次性的,一旦使用便會(huì)作廢,當(dāng)然Code也會(huì)有時(shí)效性,一般就是幾分鐘。
我們繼續(xù)看下面圖示的分解動(dòng)作吧:
首先,Authorization Server對(duì)用戶(hù)的請(qǐng)求校驗(yàn)通過(guò)后,便會(huì)分配授權(quán)碼,并通過(guò)下面這個(gè)應(yīng)答返回給用戶(hù)(瀏覽器):
HTTP/1.1?302?Found
Location:?http://my-phone-print.com:8080/oauth/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz123
用戶(hù)的瀏覽器收到這個(gè)應(yīng)答后便會(huì)重定向到Location這個(gè)地址,這個(gè)過(guò)程其實(shí)是在模擬用戶(hù)向Client(照片沖印服務(wù))提供code的行為。
當(dāng)Client(照片沖印服務(wù))收到收到用戶(hù)的code后,它會(huì)立即使用這個(gè)Code并結(jié)合自己的憑據(jù)(client_id和client_secert)向Authorization Server申請(qǐng)access_token:
POST?/oauth/token?HTTP/1.1
Host:?open.my-yunpan.com:8081
Authorization:?Basic?base64(client_id:client_secret)
Content-Type:?application/x-www-form-urlencodedgrant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=http%3A%2F%2Fmy-phone-print.com%3A8080%2Foauth%2Fcb
這是一個(gè)client發(fā)向Authorization Server的POST請(qǐng)求,請(qǐng)求參數(shù)中,除了固定的grant_type=authorization_code以及code之外,還帶了redirect_uri,這個(gè)redirect_uri是供Authorization Server校驗(yàn)使用的。此外這個(gè)請(qǐng)求是以Client身份申請(qǐng)的,所以在http header中帶上了client自己的憑據(jù)信息:client_id和client_secert,這里使用的是http basic auth。
Authorization Server對(duì)請(qǐng)求驗(yàn)證通過(guò)后,便會(huì)給出Post應(yīng)答,access_token等信息都放在應(yīng)答的包體中:
{"access_token":"2YotnFZFEjr1zCsicMWpAA","token_type":"example","expires_in":3600,"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA","example_parameter":"example_value"
}
這里除了包含access_token,還包含了它的過(guò)期時(shí)間(expires_in)以及一個(gè)refresh_token,client可以使用refresh_token在access_token過(guò)期前換取一個(gè)新的access_token。但從安全角度考慮,client不能無(wú)限制的換取新token,所以refresh_token也會(huì)被過(guò)期時(shí)間。一旦refresh_token過(guò)期了,那么client就要重新發(fā)起一次用戶(hù)授權(quán)過(guò)程。
當(dāng)client收到access_token后,便可以拿著這個(gè)access_token到Resource Server(這里是my-yunpan.com)去獲取用戶(hù)(tonybai)的個(gè)人資料與照片數(shù)據(jù)了:
POST?/photos?HTTP/1.1
Host:?my-yunpan.com:8082method=listall&access_token=2YotnFZFEjr1zCsicMWpAA
my-yunpan.com驗(yàn)證access_token后,便會(huì)將tonybai的照片列表返回給client,然后client會(huì)返回重定向應(yīng)答給用戶(hù)的瀏覽器。用戶(hù)瀏覽器收到重定向應(yīng)答后,便會(huì)向client(照片沖印服務(wù))的/photos端點(diǎn)發(fā)起請(qǐng)求,之后便可以在瀏覽器上看到自己的照片列表了。用戶(hù)選擇要沖印的照片后,創(chuàng)建訂單沖印即可。
從用戶(hù)提交code給client,到用戶(hù)瀏覽器顯示照片列表,這中間用戶(hù)可能會(huì)有短暫的等待,畢竟client要與Authorization Server和Resource server進(jìn)行多次交互,用戶(hù)瀏覽器也要進(jìn)行重定向操作。
現(xiàn)在將“分解動(dòng)作”與OAuth2.0 RFC中的授權(quán)碼方案流程圖結(jié)合在一起看,你將會(huì)對(duì)OAuth有更深刻的理解。
注:OAuth2授權(quán)流程原則上是要建立在HTTPS建立的安全通道之上的,這里僅是示例,我們聚焦的是OAuth2流程,所以將使用HTTP進(jìn)行展示。
3. 示例的具體實(shí)現(xiàn)
下面我們用Go語(yǔ)言編寫(xiě)一個(gè)可以簡(jiǎn)單演示OAuth2.0授權(quán)流程的示例,該示例與上面描述的OAuth2的“分解動(dòng)作”基本是可以對(duì)應(yīng)起來(lái)的。
示例由三個(gè)服務(wù)構(gòu)成:my-photo-print照片沖印服務(wù)、my-yunpan云盤(pán)服務(wù)以及open-my-yunpan云盤(pán)開(kāi)放平臺(tái)/授權(quán)服務(wù),示例對(duì)應(yīng)的目錄結(jié)構(gòu)如下:
$tree?-L?1?-F?oauth2-examples
oauth2-examples
├──?my-photo-print/
├──?my-yunpan/
└──?open-my-yunpan/
在開(kāi)始編寫(xiě)服務(wù)前,我們需要修改一下本機(jī)(MacOS或Linux)的/etc/hosts文件:
127.0.0.1?my-photo-print.com
127.0.0.1?my-yunpan.com
127.0.0.1?open.my-yunpan.com
注:由于示例中較少使用到j(luò)s,且form action的地址也是同源的,并且通過(guò)重定向來(lái)跳轉(zhuǎn),所以基本不涉及到跨域問(wèn)題[11]。
注:在演示下面步驟前,請(qǐng)先進(jìn)入到oauth2-examples的各個(gè)目錄下,通過(guò)go run main.go啟動(dòng)各個(gè)服務(wù)程序(每個(gè)程序一個(gè)終端窗口)。
3.1 用戶(hù)使用my-photo-print.com照片沖印服務(wù)
按照流程,用戶(hù)首先通過(guò)瀏覽器打開(kāi)照片沖印服務(wù)的首頁(yè):http://my-photo-print.com:8080,如下圖:
這個(gè)頁(yè)面是由homeHandler提供的:
//?oauth2-examples/my-photo-print/main.go//?照片沖印主頁(yè),引導(dǎo)用戶(hù)去授權(quán)平臺(tái)
func?homeHandler(w?http.ResponseWriter,?r?*http.Request)?{fmt.Println("homeHandler:",?*r)//?渲染首頁(yè)頁(yè)面模板var?state?=?randString(6)mu.Lock()stateCache[state]?=?struct{}{}mu.Unlock()tmpl?:=?template.Must(template.ParseFiles("home.html"))data?:=?map[string]interface{}{"State":?state,}tmpl.Execute(w,?data)
}
這里我們使用了服務(wù)端模板渲染,并將渲染的結(jié)果作為應(yīng)答發(fā)給瀏覽器,home.html模板的內(nèi)容如下:
//?oauth2-examples/my-photo-print/home.html<!DOCTYPE?html>
<html>
<head><title>照片沖印服務(wù)</title>
</head>
<body><h3>歡迎使用照片沖印服務(wù)!</h3><div>用戶(hù)名:?<input?name="username"/>密碼:?<input?name="password"?type="password"/>?<button>登錄</button></div><button?id="auth-btn">使用云盤(pán)賬號(hào)登錄</button><script>var?authBtn?=?document.getElementById('auth-btn');authBtn.addEventListener('click',?function()?{var?clientId?=?'my-photo-print';?var?scope?=?'user_info,read_photos';var?state?=?'{{.State}}';var?url?=?'http://open.my-yunpan.com:8081/oauth/portal?client_id='?+?clientId?+?'&scope='?+?scope+'&state='?+?state?+?'&redirect_uri=http%3A%2F%2Fmy-photo-print.com%3A8080%2Foauth%2Fcb'?window.location.href?=?url;})</script>
</body>
</html>
當(dāng)用戶(hù)選擇并點(diǎn)擊“使用云盤(pán)賬號(hào)登錄”時(shí),瀏覽器將打開(kāi)云盤(pán)開(kāi)放平臺(tái)/授權(quán)服務(wù)的首頁(yè)(http://open.my-yunpan.com:8081/oauth/portal)。
3.2 使用open.my-yunpan.com進(jìn)行授權(quán)
下面是云盤(pán)開(kāi)放平臺(tái)/授權(quán)服務(wù)的首頁(yè):
這個(gè)頁(yè)面由open.my-yunpan.com的portalHandler提供:
//?oauth2-examples/open-my-yunpan/main.gofunc?portalHandler(w?http.ResponseWriter,?r?*http.Request)?{fmt.Println("portalHandler:",?*r)//?獲取請(qǐng)求參數(shù)用于渲染應(yīng)答html頁(yè)面clientID?:=?r.FormValue("client_id")scopeTxt?:=?r.FormValue("scope")state?:=?r.FormValue("state")redirectURI?:=?r.FormValue("redirect_uri")//?渲染授權(quán)頁(yè)面模板tmpl?:=?template.Must(template.ParseFiles("portal.html"))data?:=?map[string]interface{}{"AppName":?????clientID,"Scopes":??????strings.Split(scopeTxt,?","),"ScopeTxt":????scopeTxt,"State":???????state,"RedirectURI":?redirectURI,}tmpl.Execute(w,?data)
}
和照片沖印服務(wù)首頁(yè)一樣,這里同樣使用了模板渲染的應(yīng)答頁(yè)面,對(duì)應(yīng)的portal.html模板的內(nèi)容如下:
<!DOCTYPE?html>
<html><head><title>云盤(pán)授權(quán)頁(yè)面</title></head><body><h3>云盤(pán)授權(quán)頁(yè)面</h3><p>應(yīng)用{{.AppName}}正在請(qǐng)求獲取以下權(quán)限:<ul>{{range?.Scopes}}<li>{{.}}</li>{{end}}</ul></p><form?id="authorization-form"?method="post"?action="/oauth/authorize"><div>用戶(hù)名:<input?name="username"?id="username"?/>密碼:<input?name="password"?id="password"?type="password"?/><input?type="hidden"?name="response_type"?value="code"?/><input?type="hidden"?name="client_id"?value="{{.AppName}}"?/><input?type="hidden"?name="scope"?value="{{.ScopeTxt}}"?/><input?type="hidden"?name="state"?value="{{.State}}"?/><input?type="hidden"?name="redirect_uri"?value="{{.RedirectURI}}"?/><button?type="submit">授權(quán)</button></div></form></body>
</html>
該頁(yè)面將照片沖印服務(wù)要獲得的權(quán)限以列表形式展示給用戶(hù),然后提供了一個(gè)表單,用戶(hù)填寫(xiě)用戶(hù)名和密碼后,點(diǎn)擊“授權(quán)”,瀏覽器便會(huì)向開(kāi)放平臺(tái)授權(quán)服務(wù)的"/oauth/authorize"發(fā)起post請(qǐng)求以獲取code,post請(qǐng)求攜帶了一些form參數(shù),像response_type、client_id、scope、state等。
"/oauth/authorize"端點(diǎn)由authorizeHandler負(fù)責(zé)處理:
//?oauth2-examples/open-my-yunpan/main.gofunc?authorizeHandler(w?http.ResponseWriter,?r?*http.Request)?{fmt.Println("authorizeHandler:",?*r)responsTyp?:=?r.FormValue("response_type")if?responsTyp?!=?"code"?{w.WriteHeader(http.StatusBadRequest)return}user?:=?r.FormValue("username")password?:=?r.FormValue("password")mu.Lock()v,?ok?:=?validUsers[user]if?!ok?{fmt.Println("not?found?the?user:",?user)mu.Unlock()w.WriteHeader(http.StatusNonAuthoritativeInfo)return}mu.Unlock()if?v?!=?password?{fmt.Println("invalid?password")w.WriteHeader(http.StatusNonAuthoritativeInfo)return}clientID?:=?r.FormValue("client_id")scopeTxt?:=?r.FormValue("scope")state?:=?r.FormValue("state")redirectURI?:=?r.FormValue("redirect_uri")code?:=?randString(8)mu.Lock()codeCache[code]?=?authorizeContext{clientID:????clientID,scopeTxt:????scopeTxt,state:???????state,redirectURI:?redirectURI,}mu.Unlock()unescapeURI,?_?:=?url.QueryUnescape(redirectURI)redirectURI?=?fmt.Sprintf("%s?code=%s&state=%s",?unescapeURI,?code,?state)w.Header().Add("Location",?redirectURI)w.WriteHeader(http.StatusFound)
}
authorizeHandler會(huì)對(duì)用戶(hù)進(jìn)行身份認(rèn)證,通過(guò)后,它會(huì)分配code并向?yàn)g覽器返回重定向的應(yīng)答,重定向的地址就是照片沖印服務(wù)的回調(diào)地址:http://my-photo-print.com:8080/cb?code=xxx&state=yyy。
3.3 換取access token并讀取用戶(hù)照片列表
這個(gè)重定向相當(dāng)于用戶(hù)瀏覽器向http://my-photo-print.com:8080/cb?code=xxx&state=yyy發(fā)起請(qǐng)求,為照片沖印服務(wù)提供code,該請(qǐng)求由my-photo-print的oauthCallbackHandler處理:
//?oauth2-examples/my-photo-print/main.go//?callback?handler,用戶(hù)拿到code后調(diào)用該handler
func?oauthCallbackHandler(w?http.ResponseWriter,?r?*http.Request)?{fmt.Println("oauthCallbackHandler:",?*r)code?:=?r.FormValue("code")state?:=?r.FormValue("state")mu.Lock()_,?ok?:=?stateCache[state]if?!ok?{mu.Unlock()fmt.Println("not?found?state:",?state)w.WriteHeader(http.StatusBadRequest)return}delete(stateCache,?state)mu.Unlock()//?fetch?access_token?with?codeaccessToken,?err?:=?fetchAccessToken(code)if?err?!=?nil?{fmt.Println("fetch?access_token?error:",?err)return}fmt.Println("fetch?access_token?ok:",?accessToken)//?use?access_token?to?get?user's?photo?listuser,?pl,?err?:=?getPhotoList(accessToken)if?err?!=?nil?{fmt.Println("get?photo?list?error:",?err)return}fmt.Println("get?photo?list?ok:",?pl)mu.Lock()userPhotoList[user]?=?plmu.Unlock()w.Header().Add("Location",?"/photos?user="+user)w.WriteHeader(http.StatusFound)
}
這個(gè)handler中做了很多工作,包括使用code換取access token,使用access token讀取用戶(hù)的照片列表并存儲(chǔ)在自己的存儲(chǔ)中(這里用內(nèi)存模擬,生產(chǎn)環(huán)境應(yīng)該使用數(shù)據(jù)庫(kù)服務(wù)實(shí)現(xiàn)),最后返回一個(gè)重定向應(yīng)答。
用戶(hù)瀏覽器收到重定向應(yīng)答后,會(huì)重定向訪(fǎng)問(wèn)照片沖印服務(wù)的photos端點(diǎn): http://my-photo-print.com:8080/photos?user=tonybai,以獲取該用戶(hù)的照片列表。photos端點(diǎn)的處理Handler如下:
//?oauth2-examples/my-photo-print/main.go//?待獲取到用戶(hù)照片數(shù)據(jù)后,讓用戶(hù)瀏覽器重定向到該頁(yè)面
func?listPhonesHandler(w?http.ResponseWriter,?r?*http.Request)?{fmt.Println("listPhonesHandler:",?*r)user?:=?r.FormValue("user")mu.Lock()pl,?ok?:=?userPhotoList[user]if?!ok?{mu.Unlock()fmt.Println("not?found?user:",?user)w.WriteHeader(http.StatusNotFound)return}mu.Unlock()//?渲染照片頁(yè)面模板tmpl?:=?template.Must(template.ParseFiles("photolist.html"))data?:=?map[string]interface{}{"Username":??user,"PhotoList":?pl,}tmpl.Execute(w,?data)
}
這里使用了photolist.html并結(jié)合用戶(hù)的照片列表數(shù)據(jù)一起來(lái)渲染照片列表頁(yè)面,并返回給瀏覽器:
到這里示例演示就結(jié)束了,用戶(hù)通過(guò)授權(quán)讓照片沖印服務(wù)讀取到了照片數(shù)據(jù)。
這里還有一個(gè)服務(wù)沒(méi)有提及,那就是my-yunpan.com云盤(pán)服務(wù),它的實(shí)現(xiàn)較為簡(jiǎn)單,所以這里就不贅述了。
注:生產(chǎn)中,my-yunpan.com云盤(pán)服務(wù)是要對(duì)照片沖印服務(wù)的access token進(jìn)行校驗(yàn)的,這里是演示程序,沒(méi)有引入數(shù)據(jù)庫(kù)或redis來(lái)共享access token,因此這里沒(méi)有校驗(yàn)。
4. 小結(jié)
OAuth是一種廣泛使用的開(kāi)放授權(quán)機(jī)制。它通過(guò)引入授權(quán)服務(wù)器的概念,實(shí)現(xiàn)了用戶(hù)在不共享自己的用戶(hù)名密碼情況下也能安全地向第三方應(yīng)用提供特定權(quán)限的數(shù)據(jù)訪(fǎng)問(wèn)授權(quán)。
本文通過(guò)云盤(pán)開(kāi)放平臺(tái)和第三方照片打印服務(wù)的應(yīng)用場(chǎng)景詳細(xì)說(shuō)明了OAuth出現(xiàn)的背景和解決的問(wèn)題,并結(jié)合工作流程圖和Go示例代碼,通俗易懂地介紹了OAuth2授權(quán)碼模式的整體交互流程和實(shí)現(xiàn)機(jī)制。希望大家通過(guò)對(duì)這篇文章的閱讀,能加深對(duì)OAuth2工作原理和機(jī)制的理解。
文本涉及的源碼可以在這里[12]下載。
注:鑒于本人在前端的小白水平,文中涉及的html代碼部分在大模型的幫助下完成。渲染出來(lái)的頁(yè)面比較丑陋,還望大家不要責(zé)怪:)。
注:Go社區(qū)提供了很多OAuth包可以幫助大家快速構(gòu)建OAuth2的授權(quán)服務(wù)器,比如:https://github.com/go-oauth2/oauth2等。
5. 參考資料
OAuth2 Specification[13]?- https://tools.ietf.org/html/rfc6749
《OAuth2實(shí)戰(zhàn)[14]》- https://book.douban.com/subject/30487753/
An Illustrated Guide to OAuth and OpenID Connect[15]?- https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc
《OAuth2實(shí)戰(zhàn)課[16]》
OAuth和OpenID Connect的過(guò)去、現(xiàn)在和未來(lái)[17]?- https://curity.medium.com/the-past-the-present-and-the-future-of-oauth-and-openid-connect-9b3fbf574519
“Gopher部落”知識(shí)星球[18]旨在打造一個(gè)精品Go學(xué)習(xí)和進(jìn)階社群!高品質(zhì)首發(fā)Go技術(shù)文章,“三天”首發(fā)閱讀權(quán),每年兩期Go語(yǔ)言發(fā)展現(xiàn)狀分析,每天提前1小時(shí)閱讀到新鮮的Gopher日?qǐng)?bào),網(wǎng)課、技術(shù)專(zhuān)欄、圖書(shū)內(nèi)容前瞻,六小時(shí)內(nèi)必答保證等滿(mǎn)足你關(guān)于Go語(yǔ)言生態(tài)的所有需求!2023年,Gopher部落將進(jìn)一步聚焦于如何編寫(xiě)雅、地道、可讀、可測(cè)試的Go代碼,關(guān)注代碼質(zhì)量并深入理解Go核心技術(shù),并繼續(xù)加強(qiáng)與星友的互動(dòng)。歡迎大家加入!
著名云主機(jī)服務(wù)廠(chǎng)商DigitalOcean發(fā)布最新的主機(jī)計(jì)劃,入門(mén)級(jí)Droplet配置升級(jí)為:1 core CPU、1G內(nèi)存、25G高速SSD,價(jià)格5$/月。有使用DigitalOcean需求的朋友,可以打開(kāi)這個(gè)鏈接地址[19]:https://m.do.co/c/bff6eed92687 開(kāi)啟你的DO主機(jī)之路。
Gopher Daily(Gopher每日新聞) - https://gopherdaily.tonybai.com
我的聯(lián)系方式:
微博(暫不可用):https://weibo.com/bigwhite20xx
微博2:https://weibo.com/u/6484441286
博客:tonybai.com
github: https://github.com/bigwhite
Gopher Daily歸檔 - https://github.com/bigwhite/gopherdaily

商務(wù)合作方式:撰稿、出書(shū)、培訓(xùn)、在線(xiàn)課程、合伙創(chuàng)業(yè)、咨詢(xún)、廣告合作。
參考資料
[1]?
通過(guò)實(shí)例理解Go Web身份認(rèn)證的幾種方式:?https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/
[2]?通過(guò)實(shí)例理解Web應(yīng)用授權(quán)的幾種方式:?https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/
[3]?OAuth2授權(quán):?https://tools.ietf.org/html/rfc6749
[4]?OpenID身份認(rèn)證(OIDC, OpenID Connect):?https://openid.net/specs/openid-connect-core-1_0.html
[5]?OAuth是一個(gè)開(kāi)放標(biāo)準(zhǔn):?https://en.wikipedia.org/wiki/OAuth
[6]?OAuth 1.0協(xié)議才以RFC 5849的形式正式發(fā)布:?https://www.rfc-editor.org/rfc/rfc5849
[7]?OAuth 2.0以RFC 6749發(fā)布:?https://datatracker.ietf.org/doc/html/rfc6749
[8]?OAuth 2.0的RFC協(xié)議規(guī)范:?https://datatracker.ietf.org/doc/html/rfc6749
[9]?通過(guò)實(shí)例理解Go Web身份認(rèn)證的幾種方式:?https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/
[10]?通過(guò)實(shí)例理解Web應(yīng)用授權(quán)的幾種方式:?https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/
[11]?跨域問(wèn)題:?https://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example/
[12]?這里:?https://github.com/bigwhite/experiments/tree/master/oauth2-examples
[13]?OAuth2 Specification:?https://tools.ietf.org/html/rfc6749
[14]?OAuth2實(shí)戰(zhàn):?https://book.douban.com/subject/30487753/
[15]?An Illustrated Guide to OAuth and OpenID Connect:?https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc
[16]?OAuth2實(shí)戰(zhàn)課:?https://time.geekbang.org/column/intro/100053901?code=xEq9GQzVQBD0fk2eJ2wRE811l71Ld3NxuFeQg7hN8B0%3D
[17]?OAuth和OpenID Connect的過(guò)去、現(xiàn)在和未來(lái):?https://curity.medium.com/the-past-the-present-and-the-future-of-oauth-and-openid-connect-9b3fbf574519
[18]?“Gopher部落”知識(shí)星球:?https://public.zsxq.com/groups/51284458844544
[19]?鏈接地址:?https://m.do.co/c/bff6eed92687