1. 引言
周末逛簡書,看了一篇寫的極好的文章,點擊大紅心點贊,就直接給我跳轉到登錄界面了,原來點贊是需要登錄的。
可是沒有我并沒有簡書賬號,一直使用的QQ的集成登錄。下面有一排社交登錄按鈕,我們可以用第三方社交賬號登陸即可。點擊QQ圖標,就給我跳轉到了QQ登錄授權頁面,如下圖:
從圖片上我們可以看到主要包括兩個部分,一個是左邊的用戶登錄,一個是右邊告知簡書將獲取哪些權限。輸入QQ賬號和密碼,點擊授權并登錄,就成功登錄到簡書了,并成功獲取到了我QQ的賬號和昵稱,如下圖:
簡書集成的社交登錄,大大簡化了我們的注冊登錄流程,真是一號在手上網無憂啊。
這看似簡單的集成,但背后的技術原理『OAuth2.0』可沒那么簡單,那我們廢話不多說,一探究竟吧。
2. OAuth 2.0
OAuth 2.0是用于授權的行業標準協議。OAuth 2.0取代了在2006年創建的原始OAuth協議上所做的工作。OAuth 2.0專注于客戶端開發人員的簡單性,同時為Web應用程序,桌面應用程序,手機和客廳設備提供特定的授權流程。
在傳統的client-server認證模型中,客戶端請求訪問服務器上受限的資源(protected resource),需要通過使用資源所有者(resource owner)的憑證在服務器上進行認證。為了支持第三方應用程序訪問受限資源,資源所有者需要向第三方應用共享其憑證。這就會造成以下問題:
- 第三方應用為了后續使用,會存儲資源所有者的憑證主要是密碼。
- 服務端需要支持密碼認證,盡管密碼認證不安全。
- 第三方應用獲得對資源的過度訪問而不僅局限于受限資源,且資源所有者沒有辦法對其進行限制。
- 資源所有者無法收回權限,除非修改密碼。
- 如果第三方應用的密碼被破解,就會導致所有被該密碼保護的數據被泄露。
想一想這樣一個場景,如果簡書是直接使用QQ用戶名密碼登錄,簡書就很有可能會為了后續業務的需要而擅自保存QQ用戶名及密碼,簡書只要拿到了QQ用戶名密碼就可以訪問不僅僅QQ昵稱、頭像等信息,甚至可以獲取到QQ用戶的所有通訊錄列表。如果簡書的賬號密碼泄露,就會直接影響到QQ數據的安全。這是一個可怕的問題。
所以OAuth應運而生,來解決這一問題。
3. OAuth 2.0授權流程
下面我們就以簡書使用QQ授權登錄為例,來捋一捋OAuth 2.0的流程。
先來看看OAuth 2.0的流程,如下圖所示:
這里面主要包含四個角色:
- Client:需要授權的客戶端,本文中就是【簡書】。
- Resource Owner:資源所有者,在本文中你可能會以為是 QQ,但要想清楚,QQ是屬于個人的,所以在本文中資源所有者是指【QQ用戶】。
- Authorization Server:認證服務器,本文中特指【QQ互聯平臺】。
- Resource Server:資源服務器,顧名思義,用來專門保存資源的服務器,接受通過訪問令牌進行訪問。本文特指【QQ用戶信息中心】。
3.1. 第一步:引導用戶到認證服務器
圣杰打開簡書網頁,簡書跳轉到登錄界面,要求用戶登錄。可是圣杰未在簡書注冊帳號,所以就點擊了QQ圖標,使用QQ帳號進行集成登錄。跳轉到QQ登錄界面后,QQ要求用戶授權。
這一步中簡書主要做了這樣一件事就是引導用戶到認證服務器。
很顯然【QQ互聯平臺】就是認證服務器。
如何引導?當然是頁面跳轉。
那認證服務器如何知道是簡書過來的認證請求?
當然是傳參。
那需要傳遞哪些參數呢?
- response_type:表示響應類型,必選項,此處的值固定為"code";
- client_id:表示客戶端的ID,用來標志授權請求的來源,必選項;
- redirect_uri:成功授權后的回調地址;
- scope:表示申請的權限范圍,可選項;
- state:表示客戶端的當前狀態,可以指定任意值,認證服務器會原封不動地返回這個值。
咱們看看簡書實際發送的授權請求Url是:https://graph.qq.com/oauth2.0/authorize?client_id=100410602 &redirect_uri=http://www.jianshu.com/users/auth/qq_connect/callback &response_type=code &state=bb38108d1aaf567c72da0f1167e87142d0e20cb2bb24ec5a
無圖無真相,咱們看看控制臺的網絡監控:
如圖所示,除了scope參數外,其他四個參數均有傳參。
此時你可能唯一對state參數比較迷惑,傳遞一個state參數,認證服務器會原封不動返回,那還干嘛要傳遞state參數呢?
我的理解是,簡書用一個guid加長版字符串來唯一標識一個授權請求。這樣才會正確獲取授權服務器返回的授權碼。
這里你可能會問了,既然我知道了這些參數,我豈不是可以偽造簡書認證請求,修改redirect_uri
參數跳轉到個人的網站,然后不就可以獲取QQ授權?
跟我一樣太傻太天真,簡書在QQ互聯平臺申請時肯定已經預留備案了要跳轉返回的URL。QQ互聯平臺在收到簡書的授權請求時肯定會驗證回調Url的。
3.2. 第二步:用戶同意為第三方客戶端授權
這一步,對于用戶來說,只需要使用資源所有者(QQ)的用戶名密碼登錄,并同意授權即可。點擊授權并登錄后,授權服務器首先會post一個請求回服務器進行用戶認證,認證通過后授權服務器會生成一個授權碼,然后服務器根據授權請求的redirect_uri
進行跳轉,并返回授權碼code
和授權請求中傳遞的state
。
這里要注意的是:授權碼有一個短暫的時效
無圖無真相,咱們還是看一下控制臺網絡監控:
從圖中即可驗證我們上面所說,最終跳轉回簡書的Url為:http://www.jianshu.com/users/auth/qq_connect/callback?code=093B9307E38DC5A2C3AD147B150F2AB3 &state=bb38108d1aaf567c72da0f1167e87142d0e20cb2bb24ec5a
和之前的授權請求URL進行對比,可以發現redirect_uri
、state
完全一致。
而code=093B9307E38DC5A2C3AD147B150F2AB3
就是返回的授權碼。
3.3. 第三步:使用授權碼向認證服務器申請令牌
從這一步開始,對于用戶來說是察覺不到的。簡書后臺默默的在做后續的工作。
簡書拿到QQ互聯平臺返回的授權碼后,需要根據授權碼再次向認證服務器申請令牌(access token)。
到這里有必要再理清兩個概念:
- 授權碼(Authorization Code):相當于授權服務器口頭告訴簡書,用戶同意授權使用他的QQ登錄簡書了。
- 令牌(Access Token):相當于臨時身份證。
那如何申請令牌呢?
簡書需要后臺發送一個get請求到認證服務器(QQ互聯平臺)。
那要攜帶哪些必要信息呢?
是的,要攜帶以下參數:
- grant_type:表示授權類型,此處的值固定為"authorization_code",必選項;
- client_id:表示從QQ互聯平臺申請到的客戶端ID,用來標志請求的來源,必選項;
- client_secret:這個是從QQ互聯平臺申請到的客戶端認證密鑰,機密信息十分重要,必選項;
- redirect_uri:成功申請到令牌后的回調地址;
- code:上一步申請到的授權碼。
根據以上信息我們可以模擬一個申請AccessToken的請求:https://graph.qq.com/oauth2.0/token?client_id=100410602 &client_secret=123456jianshu &redirect_uri=http://www.jianshu.com/users/auth/qq_connect/callback &grant_type=authorization_code &code=093B9307E38DC5A2C3AD147B150F2AB3
發送完該請求后,認證服務器驗證通過后就會發放令牌,并返回到簡書后臺,其中應該包含以下信息:
- access_token:令牌
- expires_in:access token的有效期,單位為秒。
- refresh_token:在授權自動續期步驟中,獲取新的Access_Token時需要提供的參數。
同樣,我們可以模擬出一個返回的token:http://www.jianshu.com/users/auth/qq_connect/callback?access_token=548ADF2D5E1C5E88H4JH15FKUN51F &expires_in=36000&refresh_token=53AD68JH834HHJF9J349FJADF3
這個時候簡書還有一件事情要做,就是把用戶token寫到cookie里,進行用戶登錄狀態的維持。咱們還是打開控制器驗證一下。
從圖中可以看出簡書把用戶token保存在名為remember_user_token
的cookie里。
不用打cookie的歪主意了,肯定是加密了的。
可以嘗試下手動把remember_user_token
這條cookie刪除,保證刷新界面后需要你重新登錄簡書。
3.4. 第四步:向資源服務器申請資源
有了token,向資源服務器提供的資源接口發送一個get請求不就行了,資源服務器校驗令牌無誤,就會向簡書返回資源(QQ用戶信息)。
同樣咱們也來模擬一個使用token請求QQ用戶基本信息資源的URL:https://graph.qq.com/user/get_user_info?client_id=100410602 &qq=2098769873 &access_token=548ADF2D5E1C5E88H4JH15FKUN51F
到這一步OAuth2.0的流程可以說是結束了,但是對于簡書來說還有重要的事情要做。那就是:
拿到token、reresh_token和用戶數據這么重要的東西不存數據庫傻呀?
3.5. 第五步:令牌延期(刷新)
你肯定對第四步返回的refresh_token
比較好奇。
它是用來對令牌進行延期(刷新)的。為什么會有兩種說法呢,因為可能認證服務器會重新生成一個令牌,也有可能
對過期的令牌進行延期。
比如說,QQ互聯平臺為了安全性考慮,返回的access_token
是有時間限制的,假如用戶某天不想授權了呢,總不能給了個access_token
你幾年后還能用吧。我們上面模擬返回的令牌有效期為10小時。10小時后,用戶打開瀏覽器逛簡書,瀏覽器中用戶的token對應的cookie已過期。簡書發現瀏覽器沒有攜帶token信息過來,就明白token失效了,需要重新向認證平臺申請授權。如果讓用戶再點擊QQ進行登錄授權,這明顯用戶體驗不好。咋搞呢?refresh_token
就派上了用場,可以直接跳過前面申請授權碼的步驟,當發現token失效了,簡書從瀏覽器攜帶的cookie中的sessionid找到存儲在數據庫中的refresh_token
,然后再使用refresh_token
進行token續期(刷新)。
那用refresh_token進行token續期需要怎么做呢?
同樣需要向認證服務器發送一個get請求。
需要哪些參數?
- grant_type:表示授權類型,此處的值固定為"refresh_token",必選項;
- client_id:表示從QQ互聯平臺申請到的客戶端ID,用來標志請求的來源,必選項;
- client_secret:這個是從QQ互聯平臺申請到的客戶端認證密鑰,機密信息十分重要,必選項;
- refresh_token:即申請令牌返回的refresh_token。
根據上述信息,我們又可以模擬一個令牌刷新的URL:https://graph.qq.com/oauth2.0/token?client_id=100410602 &client_secret=123456jianshu &redirect_uri=http://www.jianshu.com/users/auth/qq_connect/callback &grant_type=refresh_token &refresh_token=53AD68JH834HHJF9J349FJADF3
那返回的結果呢?
和第四步返回的結果一樣。
這里你可能又有疑問了,那既然每次進行令牌延期后都會重新返回一個refresh_token
,那豈不是我可以使用refresh_token
無限延期?
天真如我啊,refresh_token
也是有過期時間的。而這個過期時間具體是由認證服務器決定的。
一般來說refresh_token
的過期時間要大于access_token
的過期時間。只有這樣,access_token
過期時,才可以使用refresh_token
進行令牌延期(刷新)。
舉個簡單例子:
假設簡書從QQ互聯平臺默認獲取到的access_token
的有效期是1天,refresh_token
的有效期為一周。
用戶今天使用QQ登錄授權后,過了兩天再去逛簡書,簡書發現token失效,立馬用refresh_token
刷新令牌,默默的完成了授權的延期。
假如用戶隔了兩周再去逛簡書,簡書一核對,access_token
、refresh_token
全都失效,就只能乖乖引導用戶到授權頁面重新授權,也就是回到OAuth2.0的第一步。
4.0 總結
本文以簡書通過QQ進行授權登錄為例,對OAuth2.0 的授權流程進行了梳理,希望通讀此文,對你有所幫助。
如果對OAuth2.0有所了解的話,你應該明白本文其實是對OAuth2.0中授權碼模式授權方式的講解。
如果想了解OAuth2.0其他幾種授權方式,建議參考理解OAuth 2.0?- 阮一峰的網絡日志。