OAuth2原理與LinkedIn的第三方分享實戰

91Code發表於2019-01-16

OAuth是什麼

開放授權(OAuth)是一個開放標準,允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。在全世界得到廣泛應用,目前的版本是2.0版。

比如我的應用需要實現在領英上替使用者分享一個動態,但是隻有得到使用者的授權才能在我的應用中呼叫領英的API進行分享操作,如果直接讓使用者把使用者名稱密碼傳到我的應用,確實可以實現,但是有以下問題:

  • 為了後續的服務,我的應用會儲存使用者的密碼,在我的應用中儲存密碼很不安全
  • 使用者無法限制我的應用中能進行哪些操作
  • 領英必須要有密碼登入的功能,而單純的密碼登入並不安全
  • 我的應用中還需要處理驗證碼這樣的額外身份驗證情況
  • 使用者只有修改密碼,才能收回許可權,但會讓所有獲得使用者授權的第三方應用程式全部失效
  • 只要有一個第三方應用程式被破解,就會導致使用者密碼洩漏,以及所有被密碼保護的資料洩漏

OAuth就是為了解決上面這些問題而誕生的。

OAuth原理

 +--------+                               +---------------+
 |        |--(A)- Authorization Request ->|   Resource    |
 |        |                               |     Owner     |
 |        |<-(B)-- Authorization Grant ---|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(C)-- Authorization Grant -->| Authorization |
 | Client |                               |     Server    |
 |        |<-(D)----- Access Token -------|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(E)----- Access Token ------>|    Resource   |
 |        |                               |     Server    |
 |        |<-(F)--- Protected Resource ---|               |
 +--------+                               +---------------+
複製程式碼
  • Client:客戶端,第三方應用,我的應用就是一個Client
  • Resource Owner :使用者
  • Authorization Server:授權伺服器,即提供第三方登入服務的伺服器,如領英
  • Resource Server:服務提供商,通常和授權伺服器屬於同一應用,如領英

根據上圖的資訊,我們可以知道OAuth2的基本流程為:

  1. 使用者呼叫第三方應用的功能,如分享,該功能需要使用使用者在服務提供商上的許可權,所以第三方應用直接重定向到授權伺服器的登入、授權頁面等待使用者登入、授權。
  2. 使用者登入之後同意授權,並返回一個憑證(code)給第三方應用
  3. 第三方應用通過第二步的憑證(code)向授權伺服器請求授權
  4. 授權伺服器驗證憑證(code)通過後,同意授權,並返回一個服務訪問的憑證(Access Token)。
  5. 第三方應用通過第四步的憑證(Access Token)向服務提供商請求相關資源。
  6. 服務提供商驗證憑證(Access Token)通過後,將第三方應用請求的資源返回。

簡單來說,OAuth在"客戶端"與"服務提供商"之間,設定了一個授權層。"客戶端"不能直接登入"服務提供商",只能登入"授權層",以此將使用者與客戶端區分開來。"客戶端"登入"授權層"所用的憑證(Access Token),與使用者的密碼不同。使用者可以在登入的時候,授權伺服器指定授權層令牌的許可權範圍和有效期。

LinkedIn的OAuth2互動

下面以在我的應用上實現在領英中替使用者分享一個動態為例子,真切地來體驗一把OAuth2。

互動圖如下,細節會在下面寫到:

OAuth2原理與LinkedIn的第三方分享實戰

配置LinkedIn應用程式

除了領英,其他任意開發者平臺都需要我們在平臺上註冊一個App才能給我們呼叫平臺API的許可權,也方便平臺對我們的資質稽核以及呼叫管理。

所以首先我們得去領英開發者平臺去註冊一個App: www.linkedin.com/developer/a…

OAuth2原理與LinkedIn的第三方分享實戰

  • Keys: 完成之後會得到App相關的Key,這是在後續OAuth2互動時需要向領英提供身份驗證。
  • 許可權:在使用者登入時會給使用者展示出我的應用需要哪些許可權,我的應用最後呼叫API的時候會檢測許可權是否允許。
  • 回撥API介面:這是在OAuth2互動時需要驗證的一個引數,領英只會與已識別為可信終端的URL進行通訊。

獲取使用者授權憑證Code

使用者在我的應用中點選【分享】按鈕,會傳送一個GET請求到myApp/linkedin/auth/authorization,其中linkedin/auth/authorization是我的應用裡暴露出的一個專門用來進行驗證的介面。

我的應用收到請求之後直接回復重定向到領英的授權頁面,並且重定向的url中必須包含領英實現的OAuth2的一些引數,如下:

OAuth2原理與LinkedIn的第三方分享實戰

其中state引數是防止csrf攻擊加入進來的,關於csrf以及本例中對csrf防範的實現會在文章最後一部分提到。

在go語言+Beego框架中的實現如下:

func (c *AuthorizationController) Get() {
	host := beego.AppConfig.String("host")
	state := util.Generate(10) // random string with length 10
	clientId := beego.AppConfig.String("linkedin_client_id") // 建立app得到的key
	linkedinOauth2AuthorizationUrl := "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=%s&redirect_uri=%s/linkedin/auth/callback&state=%s"
	uri := fmt.Sprintf(linkedinOauth2AuthorizationUrl, clientId, host, state)

	c.SetSession("_csrf_Token", state)
	c.Redirect(uri, 302)
}
複製程式碼

瀏覽器跳轉到領英頁面等待授權

瀏覽器從上一步中根據重定向url跳轉到領英頁面,等待使用者登入並授權,如果使用者已經登入就直接跳轉,如下圖:

OAuth2原理與LinkedIn的第三方分享實戰

使用者如果點選Cancel,或者請求因任何其他原因而失敗,則會將其重定向回redirect_uri的URL,並附加一些錯誤引數。

使用者如果點選Allow,使用者批准您的應用程式訪問其成員資料並代表他們與LinkedIn進行互動,也會將其重定向回redirect_uri,並附加重要引數code

通過Code拿到Token,並實現分享

redirect_uri就是我的應用中的callback介面,這個介面等待使用者傳遞一個允許授權的憑證code,在我的應用中就可以拿這個code去領英中申請access token,以後就拿著這個access token去訪問領英中允許訪問的資源了

func (c *CallbackController) Get() {
	// csrf validation
	csrfToken := c.GetSession("_csrf_Token")
	if csrfToken != c.GetString("state") {
		c.Data["json"] = map[string]interface{}{
			"error":             c.GetString("csrf error"),
			"error_description": c.GetString("error_description")}
		c.ServeJSON()
		return
	}

	// user cancel authorization request or linkedin server error
	if c.GetString("error") != "" {
		c.Data["json"] = map[string]interface{}{
			"error":             c.GetString("error"),
			"error_description": c.GetString("error_description")}
		c.ServeJSON()
		return
	}

	// user accept authorization request
	code := c.GetString("code")
	if code != "" {
		// get access token by code
		accessToken := getAccessToken(code)
		// share by access token
		shareResponse := share(accessToken.AccessToken)
		c.Data["json"] = &shareResponse
		c.ServeJSON()
		return
	}
}
複製程式碼

getAccessToken和share方法僅僅是傳送請求到領英的REST API介面,實際上如果領英有golang的sdk的話,就不需要我們自己使用golang的http包來自己封裝請求處理結果了,但可惜領英並沒有。

最後給一個GIF圖,該圖包含了所有的流程展示,從點選【分享】按鈕(呼叫localhost/linkedin/auth/authorization介面)開始:

OAuth2原理與LinkedIn的第三方分享實戰

CSRF

跨站請求偽造(Cross-site request forgery), 簡稱為 CSRF,是 Web 應用中常見的一個安全問題。前面的連結也詳細講述了 CSRF 攻擊的實現方式。

當前防範 CSRF 的一種通用的方法,是對每一個使用者都記錄一個無法預知的 cookie 資料,然後要求所有提交的請求(POST/PUT/DELETE)中都必須帶有這個 cookie 資料。如果此資料不匹配 ,那麼這個請求就可能是被偽造的。

我們這裡防範CSRF的方法就是通過在使用者第一次點選分享時(呼叫/linkedin/auth/authorization)在Session中儲存一個字串:

func (c *AuthorizationController) Get() {
	...
	c.SetSession("_csrf_Token", state)
	...
}

複製程式碼

當使用者瀏覽器拿到使用者的授權憑證code之後傳送給我的應用時(呼叫/linkedin/auth/callback)檢測此時請求中的state與之前在Session中儲存的字串是否相同。


func (c *CallbackController) Get() {
	...
	csrfToken := c.GetSession("_csrf_Token")
	if csrfToken != c.GetString("state") {
		c.Data["json"] = map[string]interface{}{"error": "xsrf error"}
		c.ServeJSON()
		return
	}
	...
}
複製程式碼

如果相同則能判斷是使用者的請求,如果不同,則可能是使用者點選了其他使用者偽造的一個連結傳送的請求,從而可以防止code被髮送到惡意網站上去

本文資源

程式碼及部分圖片工程檔案:github.com/xbox1994/OA…

參考

zh.wikipedia.org/wiki/開放授權
www.ruanyifeng.com/blog/2014/0…
developer.linkedin.com/docs/share-…
zhuanlan.zhihu.com/p/20913727
www.cnblogs.com/flashsun/p/…
beego.me/docs/mvc/co…

91Code-就要編碼,關注公眾號獲取更多內容!

在這裡插入圖片描述

相關文章