轉(zhuǎn)載阮一峰Oauth2.0文章:
OAuth 2.0 的一個簡單解釋
OAuth 2.0 是目前最流行的授權(quán)機(jī)制,用來授權(quán)第三方應(yīng)用,獲取用戶數(shù)據(jù)。
這個標(biāo)準(zhǔn)比較抽象,使用了很多術(shù)語,初學(xué)者不容易理解。其實(shí)說起來并不復(fù)雜,下面我就通過一個簡單的類比,幫助大家輕松理解,OAuth 2.0 到底是什么。
一、快遞員問題
我住在一個大型的居民小區(qū)。

小區(qū)有門禁系統(tǒng)。

進(jìn)入的時候需要輸入密碼。

我經(jīng)常網(wǎng)購和外賣,每天都有快遞員來送貨。我必須找到一個辦法,讓快遞員通過門禁系統(tǒng),進(jìn)入小區(qū)。

如果我把自己的密碼,告訴快遞員,他就擁有了與我同樣的權(quán)限,這樣好像不太合適。萬一我想取消他進(jìn)入小區(qū)的權(quán)力,也很麻煩,我自己的密碼也得跟著改了,還得通知其他的快遞員。
有沒有一種辦法,讓快遞員能夠自由進(jìn)入小區(qū),又不必知道小區(qū)居民的密碼,而且他的唯一權(quán)限就是送貨,其他需要密碼的場合,他都沒有權(quán)限?
二、授權(quán)機(jī)制的設(shè)計(jì)
于是,我設(shè)計(jì)了一套授權(quán)機(jī)制。
第一步,門禁系統(tǒng)的密碼輸入器下面,增加一個按鈕,叫做"獲取授權(quán)"??爝f員需要首先按這個按鈕,去申請授權(quán)。
第二步,他按下按鈕以后,屋主(也就是我)的手機(jī)就會跳出對話框:有人正在要求授權(quán)。系統(tǒng)還會顯示該快遞員的姓名、工號和所屬的快遞公司。
我確認(rèn)請求屬實(shí),就點(diǎn)擊按鈕,告訴門禁系統(tǒng),我同意給予他進(jìn)入小區(qū)的授權(quán)。
第三步,門禁系統(tǒng)得到我的確認(rèn)以后,向快遞員顯示一個進(jìn)入小區(qū)的令牌(access token)。令牌就是類似密碼的一串?dāng)?shù)字,只在短期內(nèi)(比如七天)有效。
第四步,快遞員向門禁系統(tǒng)輸入令牌,進(jìn)入小區(qū)。
有人可能會問,為什么不是遠(yuǎn)程為快遞員開門,而要為他單獨(dú)生成一個令牌?這是因?yàn)榭爝f員可能每天都會來送貨,第二天他還可以復(fù)用這個令牌。另外,有的小區(qū)有多重門禁,快遞員可以使用同一個令牌通過它們。
三、互聯(lián)網(wǎng)場景
我們把上面的例子搬到互聯(lián)網(wǎng),就是 OAuth 的設(shè)計(jì)了。
首先,居民小區(qū)就是儲存用戶數(shù)據(jù)的網(wǎng)絡(luò)服務(wù)。比如,微信儲存了我的好友信息,獲取這些信息,就必須經(jīng)過微信的"門禁系統(tǒng)"。
其次,快遞員(或者說快遞公司)就是第三方應(yīng)用,想要穿過門禁系統(tǒng),進(jìn)入小區(qū)。
最后,我就是用戶本人,同意授權(quán)第三方應(yīng)用進(jìn)入小區(qū),獲取我的數(shù)據(jù)。
簡單說,OAuth 就是一種授權(quán)機(jī)制。數(shù)據(jù)的所有者告訴系統(tǒng),同意授權(quán)第三方應(yīng)用進(jìn)入系統(tǒng),獲取這些數(shù)據(jù)。系統(tǒng)從而產(chǎn)生一個短期的進(jìn)入令牌(token),用來代替密碼,供第三方應(yīng)用使用。
四、令牌與密碼
令牌(token)與密碼(password)的作用是一樣的,都可以進(jìn)入系統(tǒng),但是有三點(diǎn)差異。
(1)令牌是短期的,到期會自動失效,用戶自己無法修改。密碼一般長期有效,用戶不修改,就不會發(fā)生變化。
(2)令牌可以被數(shù)據(jù)所有者撤銷,會立即失效。以上例而言,屋主可以隨時取消快遞員的令牌。密碼一般不允許被他人撤銷。
(3)令牌有權(quán)限范圍(scope),比如只能進(jìn)小區(qū)的二號門。對于網(wǎng)絡(luò)服務(wù)來說,只讀令牌就比讀寫令牌更安全。密碼一般是完整權(quán)限。
上面這些設(shè)計(jì),保證了令牌既可以讓第三方應(yīng)用獲得權(quán)限,同時又隨時可控,不會危及系統(tǒng)安全。這就是 OAuth 2.0 的優(yōu)點(diǎn)。
注意,只要知道了令牌,就能進(jìn)入系統(tǒng)。系統(tǒng)一般不會再次確認(rèn)身份,所以令牌必須保密,泄漏令牌與泄漏密碼的后果是一樣的。 這也是為什么令牌的有效期,一般都設(shè)置得很短的原因。
OAuth 2.0 對于如何頒發(fā)令牌的細(xì)節(jié),規(guī)定得非常詳細(xì)。具體來說,一共分成四種授權(quán)類型(authorization grant),即四種頒發(fā)令牌的方式,適用于不同的互聯(lián)網(wǎng)場景。下一篇文章,我就來介紹這四種類型,并給出代碼實(shí)例。
OAuth 2.0 的四種方式
上一篇文章介紹了 OAuth 2.0 是一種授權(quán)機(jī)制,主要用來頒發(fā)令牌(token)。本文接著介紹頒發(fā)令牌的實(shí)務(wù)操作。

下面我假定,你已經(jīng)理解了 OAuth 2.0 的含義和設(shè)計(jì)思想,否則請先閱讀這個系列的上一篇文章。
進(jìn)入正文之前,插播一則活動消息。
4月22日(周一)到4月29日(下周一),每天晚上八點(diǎn)都有兩小時的免費(fèi)直播課,體系化介紹高級前端開發(fā)知識,網(wǎng)易云課堂主辦。詳細(xì)介紹請看本文結(jié)尾,歡迎關(guān)注。
RFC 6749
OAuth 2.0 的標(biāo)準(zhǔn)是 RFC 6749 文件。該文件先解釋了 OAuth 是什么。
OAuth 引入了一個授權(quán)層,用來分離兩種不同的角色:客戶端和資源所有者。......資源所有者同意以后,資源服務(wù)器可以向客戶端頒發(fā)令牌。客戶端通過令牌,去請求數(shù)據(jù)。
這段話的意思就是,OAuth 的核心就是向第三方應(yīng)用頒發(fā)令牌。然后,RFC 6749 接著寫道:
(由于互聯(lián)網(wǎng)有多種場景,)本標(biāo)準(zhǔn)定義了獲得令牌的四種授權(quán)方式(authorization grant )。
也就是說,OAuth 2.0 規(guī)定了四種獲得令牌的流程。你可以選擇最適合自己的那一種,向第三方應(yīng)用頒發(fā)令牌。下面就是這四種授權(quán)方式。
- 授權(quán)碼(authorization-code)
- 隱藏式(implicit)
- 密碼式(password):
- 客戶端憑證(client credentials)
注意,不管哪一種授權(quán)方式,第三方應(yīng)用申請令牌之前,都必須先到系統(tǒng)備案,說明自己的身份,然后會拿到兩個身份識別碼:客戶端 ID(client ID)和客戶端密鑰(client secret)。這是為了防止令牌被濫用,沒有備案過的第三方應(yīng)用,是不會拿到令牌的。
第一種授權(quán)方式:授權(quán)碼
授權(quán)碼(authorization code)方式,指的是第三方應(yīng)用先申請一個授權(quán)碼,然后再用該碼獲取令牌。
這種方式是最常用的流程,安全性也最高,它適用于那些有后端的 Web 應(yīng)用。授權(quán)碼通過前端傳送,令牌則是儲存在后端,而且所有與資源服務(wù)器的通信都在后端完成。這樣的前后端分離,可以避免令牌泄漏。
第一步,A 網(wǎng)站提供一個鏈接,用戶點(diǎn)擊后就會跳轉(zhuǎn)到 B 網(wǎng)站,授權(quán)用戶數(shù)據(jù)給 A 網(wǎng)站使用。下面就是 A 網(wǎng)站跳轉(zhuǎn) B 網(wǎng)站的一個示意鏈接。
https://b.com/oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type參數(shù)表示要求返回授權(quán)碼(code),client_id參數(shù)讓 B 知道是誰在請求,redirect_uri參數(shù)是 B 接受或拒絕請求后的跳轉(zhuǎn)網(wǎng)址,scope參數(shù)表示要求的授權(quán)范圍(這里是只讀)。

第二步,用戶跳轉(zhuǎn)后,B 網(wǎng)站會要求用戶登錄,然后詢問是否同意給予 A 網(wǎng)站授權(quán)。用戶表示同意,這時 B 網(wǎng)站就會跳回redirect_uri參數(shù)指定的網(wǎng)址。跳轉(zhuǎn)時,會傳回一個授權(quán)碼,就像下面這樣。
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code參數(shù)就是授權(quán)碼。

第三步,A 網(wǎng)站拿到授權(quán)碼以后,就可以在后端,向 B 網(wǎng)站請求令牌。
https://b.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL
上面 URL 中,client_id參數(shù)和client_secret參數(shù)用來讓 B 確認(rèn) A 的身份(client_secret參數(shù)是保密的,因此只能在后端發(fā)請求),grant_type參數(shù)的值是AUTHORIZATION_CODE,表示采用的授權(quán)方式是授權(quán)碼,code參數(shù)是上一步拿到的授權(quán)碼,redirect_uri參數(shù)是令牌頒發(fā)后的回調(diào)網(wǎng)址。

第四步,B 網(wǎng)站收到請求以后,就會頒發(fā)令牌。具體做法是向redirect_uri指定的網(wǎng)址,發(fā)送一段 JSON 數(shù)據(jù)。
{ "access_token":"ACCESS_TOKEN", "token_type":"bearer", "expires_in":2592000, "refresh_token":"REFRESH_TOKEN", "scope":"read", "uid":100101, "info":{...} }
上面 JSON 數(shù)據(jù)中,access_token字段就是令牌,A 網(wǎng)站在后端拿到了。

第二種方式:隱藏式
有些 Web 應(yīng)用是純前端應(yīng)用,沒有后端。這時就不能用上面的方式了,必須將令牌儲存在前端。RFC 6749 就規(guī)定了第二種方式,允許直接向前端頒發(fā)令牌。這種方式?jīng)]有授權(quán)碼這個中間步驟,所以稱為(授權(quán)碼)"隱藏式"(implicit)。
第一步,A 網(wǎng)站提供一個鏈接,要求用戶跳轉(zhuǎn)到 B 網(wǎng)站,授權(quán)用戶數(shù)據(jù)給 A 網(wǎng)站使用。
https://b.com/oauth/authorize? response_type=token& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type參數(shù)為token,表示要求直接返回令牌。
第二步,用戶跳轉(zhuǎn)到 B 網(wǎng)站,登錄后同意給予 A 網(wǎng)站授權(quán)。這時,B 網(wǎng)站就會跳回redirect_uri參數(shù)指定的跳轉(zhuǎn)網(wǎng)址,并且把令牌作為 URL 參數(shù),傳給 A 網(wǎng)站。
https://a.com/callback#token=ACCESS_TOKEN
上面 URL 中,token參數(shù)就是令牌,A 網(wǎng)站因此直接在前端拿到令牌。
注意,令牌的位置是 URL 錨點(diǎn)(fragment),而不是查詢字符串(querystring),這是因?yàn)?OAuth 2.0 允許跳轉(zhuǎn)網(wǎng)址是 HTTP 協(xié)議,因此存在"中間人攻擊"的風(fēng)險,而瀏覽器跳轉(zhuǎn)時,錨點(diǎn)不會發(fā)到服務(wù)器,就減少了泄漏令牌的風(fēng)險。

這種方式把令牌直接傳給前端,是很不安全的。因此,只能用于一些安全要求不高的場景,并且令牌的有效期必須非常短,通常就是會話期間(session)有效,瀏覽器關(guān)掉,令牌就失效了。
第三種方式:密碼式
如果你高度信任某個應(yīng)用,RFC 6749 也允許用戶把用戶名和密碼,直接告訴該應(yīng)用。該應(yīng)用就使用你的密碼,申請令牌,這種方式稱為"密碼式"(password)。
第一步,A 網(wǎng)站要求用戶提供 B 網(wǎng)站的用戶名和密碼。拿到以后,A 就直接向 B 請求令牌。
https://oauth.b.com/token? grant_type=password& username=USERNAME& password=PASSWORD& client_id=CLIENT_ID
上面 URL 中,grant_type參數(shù)是授權(quán)方式,這里的password表示"密碼式",username和password是 B 的用戶名和密碼。
第二步,B 網(wǎng)站驗(yàn)證身份通過后,直接給出令牌。注意,這時不需要跳轉(zhuǎn),而是把令牌放在 JSON 數(shù)據(jù)里面,作為 HTTP 回應(yīng),A 因此拿到令牌。
這種方式需要用戶給出自己的用戶名/密碼,顯然風(fēng)險很大,因此只適用于其他授權(quán)方式都無法采用的情況,而且必須是用戶高度信任的應(yīng)用。
第四種方式:憑證式
最后一種方式是憑證式(client credentials),適用于沒有前端的命令行應(yīng)用,即在命令行下請求令牌。
第一步,A 應(yīng)用在命令行向 B 發(fā)出請求。
https://oauth.b.com/token? grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
上面 URL 中,grant_type參數(shù)等于client_credentials表示采用憑證式,client_id和client_secret用來讓 B 確認(rèn) A 的身份。
第二步,B 網(wǎng)站驗(yàn)證通過以后,直接返回令牌。
這種方式給出的令牌,是針對第三方應(yīng)用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。
令牌的使用
A 網(wǎng)站拿到令牌以后,就可以向 B 網(wǎng)站的 API 請求數(shù)據(jù)了。
此時,每個發(fā)到 API 的請求,都必須帶有令牌。具體做法是在請求的頭信息,加上一個Authorization字段,令牌就放在這個字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \ "https://api.b.com"
上面命令中,ACCESS_TOKEN就是拿到的令牌。
更新令牌
令牌的有效期到了,如果讓用戶重新走一遍上面的流程,再申請一個新的令牌,很可能體驗(yàn)不好,而且也沒有必要。OAuth 2.0 允許用戶自動更新令牌。
具體方法是,B 網(wǎng)站頒發(fā)令牌的時候,一次性頒發(fā)兩個令牌,一個用于獲取數(shù)據(jù),另一個用于獲取新的令牌(refresh token 字段)。令牌到期前,用戶使用 refresh token 發(fā)一個請求,去更新令牌。
https://b.com/oauth/token? grant_type=refresh_token& client_id=CLIENT_ID& client_secret=CLIENT_SECRET& refresh_token=REFRESH_TOKEN
上面 URL 中,grant_type參數(shù)為refresh_token表示要求更新令牌,client_id參數(shù)和client_secret參數(shù)用于確認(rèn)身份,refresh_token參數(shù)就是用于更新令牌的令牌。
B 網(wǎng)站驗(yàn)證通過以后,就會頒發(fā)新的令牌。
GitHub OAuth 第三方登錄示例教程
這組 OAuth 系列教程,第一篇介紹了基本概念,第二篇介紹了獲取令牌的四種方式,今天演示一個實(shí)例,如何通過 OAuth 獲取 API 數(shù)據(jù)。
很多網(wǎng)站登錄時,允許使用第三方網(wǎng)站的身份,這稱為"第三方登錄"。

下面就以 GitHub 為例,寫一個最簡單的應(yīng)用,演示第三方登錄。
一、第三方登錄的原理
所謂第三方登錄,實(shí)質(zhì)就是 OAuth 授權(quán)。用戶想要登錄 A 網(wǎng)站,A 網(wǎng)站讓用戶提供第三方網(wǎng)站的數(shù)據(jù),證明自己的身份。獲取第三方網(wǎng)站的身份數(shù)據(jù),就需要 OAuth 授權(quán)。
舉例來說,A 網(wǎng)站允許 GitHub 登錄,背后就是下面的流程。
- A 網(wǎng)站讓用戶跳轉(zhuǎn)到 GitHub。
- GitHub 要求用戶登錄,然后詢問"A 網(wǎng)站要求獲得 xx 權(quán)限,你是否同意?"
- 用戶同意,GitHub 就會重定向回 A 網(wǎng)站,同時發(fā)回一個授權(quán)碼。
- A 網(wǎng)站使用授權(quán)碼,向 GitHub 請求令牌。
- GitHub 返回令牌.
- A 網(wǎng)站使用令牌,向 GitHub 請求用戶數(shù)據(jù)。
下面就是這個流程的代碼實(shí)現(xiàn)。
二、應(yīng)用登記
一個應(yīng)用要求 OAuth 授權(quán),必須先到對方網(wǎng)站登記,讓對方知道是誰在請求。
所以,你要先去 GitHub 登記一下。當(dāng)然,我已經(jīng)登記過了,你使用我的登記信息也可以,但為了完整走一遍流程,還是建議大家自己登記。這是免費(fèi)的。
訪問這個網(wǎng)址,填寫登記表。

應(yīng)用的名稱隨便填,主頁 URL 填寫http://localhost:8080,跳轉(zhuǎn)網(wǎng)址填寫 http://localhost:8080/oauth/redirect。
提交表單以后,GitHub 應(yīng)該會返回客戶端 ID(client ID)和客戶端密鑰(client secret),這就是應(yīng)用的身份識別碼。
三、示例倉庫
我寫了一個代碼倉庫,請將它克隆到本地。
$ git clone git@github.com:ruanyf/node-oauth-demo.git $ cd node-oauth-demo
兩個配置項(xiàng)要改一下,寫入上一步的身份識別碼。
index.js:改掉變量clientIDandclientSecretpublic/index.html:改掉變量client_id
然后,安裝依賴。
$ npm install
啟動服務(wù)。
$ node index.js
瀏覽器訪問http://localhost:8080,就可以看到這個示例了。
四、瀏覽器跳轉(zhuǎn) GitHub
示例的首頁很簡單,就是一個鏈接,讓用戶跳轉(zhuǎn)到 GitHub。

跳轉(zhuǎn)的 URL 如下。
https://github.com/login/oauth/authorize? client_id=7e015d8ce32370079895& redirect_uri=http://localhost:8080/oauth/redirect
這個 URL 指向 GitHub 的 OAuth 授權(quán)網(wǎng)址,帶有兩個參數(shù):client_id告訴 GitHub 誰在請求,redirect_uri是稍后跳轉(zhuǎn)回來的網(wǎng)址。
用戶點(diǎn)擊到了 GitHub,GitHub 會要求用戶登錄,確保是本人在操作。
五、授權(quán)碼
登錄后,GitHub 詢問用戶,該應(yīng)用正在請求數(shù)據(jù),你是否同意授權(quán)。

用戶同意授權(quán), GitHub 就會跳轉(zhuǎn)到redirect_uri指定的跳轉(zhuǎn)網(wǎng)址,并且?guī)鲜跈?quán)碼,跳轉(zhuǎn)回來的 URL 就是下面的樣子。
http://localhost:8080/oauth/redirect? code=859310e7cecc9196f4af
后端收到這個請求以后,就拿到了授權(quán)碼(code參數(shù))。
六、后端實(shí)現(xiàn)
這里的關(guān)鍵是針對/oauth/redirect的請求,編寫一個路由,完成 OAuth 認(rèn)證。
const oauth = async ctx => { // ... }; app.use(route.get('/oauth/redirect', oauth));
上面代碼中,oauth函數(shù)就是路由的處理函數(shù)。下面的代碼都寫在這個函數(shù)里面。
路由函數(shù)的第一件事,是從 URL 取出授權(quán)碼。
const requestToken = ctx.request.query.code;
七、令牌
后端使用這個授權(quán)碼,向 GitHub 請求令牌。
const tokenResponse = await axios({ method: 'post', url: 'https://github.com/login/oauth/access_token?' + `client_id=${clientID}&` + `client_secret=${clientSecret}&` + `code=${requestToken}`, headers: { accept: 'application/json' } });
上面代碼中,GitHub 的令牌接口https://github.com/login/oauth/access_token需要提供三個參數(shù)。
client_id:客戶端的 IDclient_secret:客戶端的密鑰code:授權(quán)碼
作為回應(yīng),GitHub 會返回一段 JSON 數(shù)據(jù),里面包含了令牌accessToken。
const accessToken = tokenResponse.data.access_token;
八、API 數(shù)據(jù)
有了令牌以后,就可以向 API 請求數(shù)據(jù)了。
const result = await axios({ method: 'get', url: `https://api.github.com/user`, headers: { accept: 'application/json', Authorization: `token ${accessToken}` } });
上面代碼中,GitHub API 的地址是https://api.github.com/user,請求的時候必須在 HTTP 頭信息里面帶上令牌Authorization: token 361507da。
然后,就可以拿到用戶數(shù)據(jù),得到用戶的身份。
const name = result.data.name; ctx.response.redirect(`/welcome.html?name=${name}`);