01.30 微服務統一認證與授權的 Go 語言實現

各位讀者朋友鼠年大吉,祝各位新的一年身體健康,萬事如意!

最近疫情嚴重,是一個特殊時期,大家一定要注意防護。很多省份推遲了企業開工的時間,大部分的互聯網公司也都是下週開始遠程辦公。大家可以利用在家的幾天時間學習充電,反正也出不去()。

今天筆者要寫得是 Go 微服務相關的組件實踐,筆者在好幾年前就接觸 Go 語言,去年開始從事 Go 微服務相關的開發,在過程中也和小夥伴聯合編寫了一本 《Go 高併發與微服務實戰》書籍,即將出版上市。本文是截取其中的搶先版閱覽,介紹微服務統一認證與授權的 Go 語言實現。

1 前言

統一認證與授權是微服務架構的基礎功能,微服務架構不同於單體應用的架構,認證和授權非常集中。當服務拆分之後,對各個微服務認證與授權變得非常分散,所以在微服務架構中,將集成統一認證與授權的功能,作為橫切關注點。

常見的認證與授權方案有 OAuth、分佈式 Session、OpenID 和 JWT 等,下面我們將分別介紹這四種方案。

2.1 OAuth

OAuth2 相關理論的介紹主要來自於OAuth2官方文檔,相關地址為https://tools.ietf.org/html/rfc6749。

OAuth 協議的目的是為了為用戶資源的授權提供一個安全的、開放而簡易的標準。官網中的介紹如下:

An open protocol to allow secure API authorization in a simple and standard method from web, mobile and desktop applications.

OAuth1 由於不被 OAuth2 兼容,且簽名邏輯過於複雜和授權流程的過於單一,在此不過多談論,以下重點關注OAuth2認證流程,它是當前Web應用中的主流授權流程。

OAuth2是當前授權的行業標準,其重點在於為Web應用程序、桌面應用程序、移動設備以及室內設備的授權流程提供簡單的客戶端開發方式。它為第三方應用提供對HTTP服務的有限訪問,既可以是資源擁有者通過授權允許第三方應用獲取HTTP服務,也可以是第三方以自己的名義獲取訪問權限。

角色

OAuth2 中主要分為了4種角色

  • resource owner 資源所有者,是能夠對受保護的資源授予訪問權限的實體,可以是一個用戶,這時會被稱為end-user。
  • resource server 資源服務器,持有受保護的資源,允許持有訪問令牌(access token)的請求訪問受保護資源。
  • client 客戶端,持有資源所有者的授權,代表資源所有者對受保護資源進行訪問。
  • authorization server 授權服務器,對資源所有者的授權進行認證,成功後向客戶端發送訪問令牌。

在很多時候,資源服務器和授權服務器是合二為一的,在授權交互的時候是授權服務器,在請求資源交互是資源服務器。但是授權服務器是單獨的實體,它可以發出被多個資源服務器接受的訪問令牌。

協議流程

首先看一張來自官方提供的流程圖:


微服務統一認證與授權的 Go 語言實現

協議流程


這是一張關於OAuth2角色的抽象交互流程圖,主要包含以下的6個步驟:

  1. 客戶端請求資源所有者的授權;
  2. 資源所有者同意授權,返回授權許可(Authorization Grant),這代表了資源所有者的授權憑證;
  3. 客戶端攜帶授權許可要求授權服務器進行認證,請求訪問令牌;
  4. 授權服務器對客戶端進行身份驗證,並認證授權許可,如果有效,返回訪問令牌;
  5. 客戶端攜帶訪問許可向資源服務器請求受保護資源的訪問;
  6. 資源服務器驗證訪問令牌,如果有效,接受訪問請求,返回受保護資源。客戶端授權類型

為了獲取訪問令牌,客戶端必須獲取到資源所有者的授權許可。OAuth2默認定了四種授權類型,當然也提供了用於定義額外的授權類型的擴展機制。默認的四種授權類型為:

  • authorization code 授權碼類型
  • implicit 簡化類型(也稱為隱式類型)
  • resource owner password credentials 密碼類型
  • client credential 客戶端類型

下面對常用的授權碼類型和密碼類型進行詳細的介紹。

授權碼類型(authorization code)通過重定向的方式讓資源所有者直接與授權服務器進行交互來進行授權,避免了資源所有者信息洩漏給客戶端,是功能最完整、流程最嚴密的授權類型,但是需要客戶端必須能與資源所有者的代理(通常是Web瀏覽器)進行交互,和可從授權服務器中接受請求(重定向給予授權碼),授權流程如下:客戶端引導資源所有者的用戶代理到授權服務器的endpoint,一般通過重定向的方式。客戶端提交的信息應包含客戶端標識(client identifier)、請求範圍(requested scope)、本地狀態(local state)和用於返回授權碼的重定向地址(redirection URI);


微服務統一認證與授權的 Go 語言實現


  1. 授權服務器認證資源所有者(通過用戶代理),並確認資源所有者允許還是拒絕客戶端的訪問請求;
  2. 如果資源所有者授予客戶端訪問權限,授權服務器通過重定向用戶代理的方式回調客戶端提供的重定向地址,並在重定向地址中添加授權碼和客戶端先前提供的任何本地狀態;
  3. 客戶端攜帶上一步獲得的授權碼向授權服務器請求訪問令牌。在這一步中授權碼和客戶端都要被授權服務器進行認證。客戶端需要提交用於獲取授權碼的重定向地址;
  4. 授權服務器對客戶端進行身份驗證,和認證授權碼,確保接收到的重定向地址與第三步中用於的獲取授權碼的重定向地址相匹配。如果有效,返回訪問令牌,以及可能返回的刷新令牌(Refresh Token)。

密碼類型

密碼類型(resource owner password credentials)需要資源所有者將密碼憑證交予客戶端,客戶端通過自己持有的信息直接向授權服務器獲取授權。在這種情況下,需要資源所有者對客戶端高度可信任,同時客戶端不允許保存密碼憑證。這種授權類型適用於能夠獲取資源所有者的憑證(credentials)(如用戶名和密碼)的客戶端。授權流程如下:


微服務統一認證與授權的 Go 語言實現

密碼類型

  1. 資源所有者向客戶端提供其用戶名和密碼等憑證;
  2. 客戶端攜帶資源所有者的憑證(用戶名和密碼),向授權服務器請求訪問令牌;
  3. 授權服務器認證客戶端並且驗證資源所有者的憑證,如果有效,返回訪問令牌,以及可能返回的刷新令牌(Refresh Token)。

令牌刷新

客戶端從授權服務器中獲取的訪問令牌(access token)一般是具備失效性的,在訪問令牌過期的情況下,持有有效用戶憑證的客戶端可以再次向授權服務器請求訪問令牌,但是如果不持有用戶憑證的客戶端可以通過和上次訪問令牌一同返回的刷新令牌(refresh token)向授權服務器獲取新的訪問令牌。

2.2 分佈式 Session

2.2.1 什麼是 Session,什麼是 Cookie?

HTTP 協議是無狀態的協議。一旦數據交換完畢,客戶端與服務器端的連接就會關閉,再次交換數據需要建立新的連接。這就意味著服務器無法從連接上跟蹤會話。

會話,指用戶登錄網站後的一系列動作,比如瀏覽商品添加到購物車併購買。會話(Session)跟蹤是 Web 程序中常用的技術,用來跟蹤用戶的整個會話。常用的會話跟蹤技術是 Cookie 與 Session。

Cookie 實際上是一小段的文本信息。客戶端請求服務器,如果服務器需要記錄該用戶狀態,就使用 response 向客戶端瀏覽器頒發一個 Cookie。客戶端會把 Cookie 保存起來。

當瀏覽器再請求該網站時,瀏覽器把請求的網址連同該 Cookie 一同提交給服務器。服務器檢查該 Cookie,以此來辨認用戶狀態。服務器還可以根據需要修改 Cookie 的內容。

Session 是另一種記錄客戶狀態的機制,不同的是 Cookie 保存在客戶端瀏覽器中,而 Session 保存在服務器上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄

在服務器上。這就是 Session。客戶端瀏覽器再次訪問時只需要從該 Session 中查找該客戶的狀態就可以了。

每個用戶訪問服務器都會建立一個 session,那服務器是怎麼標識用戶的唯一身份呢?事實上,用戶與服務器建立連接的同時,服務器會自動為其分配一個 SessionId。

簡單來說,Cookie 通過在客戶端記錄信息確定用戶身份,Session通過在服務器端記錄信息確定用戶身份。

2.3 OpenID

某些站點看到允許以 OpenID 的方式登陸,如使用 Facebook 賬號或者 Google 賬號登陸站點。

OpenID 和 OAuth 很像。但本質上來說它們是截然不同的兩個東西:

  • OpenID: 只用於 身份認證(Authentication),允許你以 同一個賬戶 在 多個網站登陸。它僅僅是為你的 合法身份 背書,當你以 Facebook 賬號登陸某個站點之後,該站點 無權訪問 你的在 Facebook 上的 數據。
  • OAuth: 用於 授權(Authorisation),允許 被授權方 訪問 授權方 的 用戶數據。

2.4 JWT

JWT,JSON Web Token,作為一個開放的標準,通過緊湊(compact,快速傳輸,體積小)或者自包含(self-contained,payload中將包含用戶所需的所有的信息,避免了對數據庫的多次查詢)的方式,定義了用於在各方之間發送的安全JSON對象。

為什麼要介紹JWT,因為JWT可以很好的充當在上一節介紹的訪問令牌(access token)和刷新令牌(refresh token)的載體,這是Web雙方之間進行安全傳輸信息的良好方式。當只有授權服務器持有簽發和驗證JWT的secret,那麼就只有授權服務器能驗證JWT的有效性以及發送帶有簽名的JWT,這就唯一保證了以JWT為載體的token的有效性和安全性。

JWT的組成

JWT格式一般如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0.IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs

它由三部分組成,每部分通過.分隔開,分別是:

  • Header 頭部
  • Payload 有效負荷
  • Signature 簽名

接著我們對每一部分進行詳細的介紹。

Header

頭部通常由兩部分組成:

  • typ 類型,一般為jwt。
  • alg 加密算法,通常是HMAC SHA256或者RSA。

一個簡單的頭部例子如下:

<code>{"alg":"HS256""typ":"JWT"}/<code>

然後這部分JSON會被Base64Url編碼用於構成JWT的第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Playload

有效負載是JWT的第二部分,是用來攜帶有效信息的載體,主要是關於用戶實體和附加元數據的聲明,由以下三部分組成:

  • Registered claims 註冊聲明,這是一組預定的聲明,但並不強制要求,提供了一套有用的、能共同使用的聲明。主要有iss(JWT簽發者),exp(JWT過期時間),sub(JWT面向的用戶),aud(接受JWT的一方)等。
  • Public claims 公開聲明 公開聲明中可以添加任何信息,一般是用戶信息或者業務擴展信息等。
  • Private claims 私有聲明 被JWT提供者和消費者共同定義的聲明,既不屬於註冊聲明也不屬於公開聲明。

一般不建議在payload中添加任何的敏感信息,因為Base64是對稱解密的,這意味著payload中的信息的是可見的。

一個簡單的有效負荷例子:

<code>{"name":"cangwu","exp":1518051157,"userId":"123456"}/<code>

這部分JSON會被Base64Url編碼用於構成JWT的第二部分:

eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0

Signature

要創建簽名,必須需要被編碼後的頭部、被編碼後的有效負荷、一個secret,最後通過在頭部的定義的加密算法alg加密生成簽名,生成簽名的偽代碼如下:

<code>HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)/<code>

用到的加密算法為HMACSHA256

secret是保存在服務端用於驗證JWT以及簽發JWT,所以必須只由服務端持有,不該流露出去。

一個簡單的簽名如下:

IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs

這將成為JWT的第三部分。

最後這三部分通過.分割,組成最終的JWT,如下所示:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0.IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs

3.1 整體架構

經過以上的簡單介紹,我們已經瞭解了目前常見的統一認證與鑑權的方案,接下來我們將基於 OAuth2 協議和 JWT 實現一套簡單的認證和授權系統。系統主要由兩個服務組成,授權服務器和資源服務器,它們之間的交互圖 11-4 所示:

微服務統一認證與授權的 Go 語言實現

客戶端想要訪問資源服務器中用戶持有的資源信息,首先需要攜帶用戶憑證向授權服務器請求訪問令牌。授權服務器在驗證過客戶端和用戶憑證的有效性後,它將返回生成的訪問令牌給客戶端。接著客戶端攜帶訪問令牌向資源服務器請求對應的用戶資源,在資源服務器通過授權服務器驗證過訪問令牌有效後,將返回對應的用戶資源。

很多時候,授權服務器和資源服務器是合二為一,即可以頒發訪問令牌,也對用戶資源受限訪問;也可以將它們的職責劃分得更加詳細,授權服務器主要負責令牌的頒發和令牌的驗證,而資源服務器負責對用戶資源進行保護,僅允許持有有效訪問令牌的請求訪問受限資源。

授權服務器的主要職責有頒發訪問令牌和驗證訪問令牌,對此我們需要對外提供兩個接口:

  • /oauth/token 用於客戶端攜帶用戶憑證請求訪問令牌
  • /oauth/check_token 用於驗證訪問令牌的有效性,返回訪問令牌對應的客戶端和用戶信息。

一般來講,每一個客戶端都可以為用戶申請訪問令牌,因此一個有效的訪問令牌是和客戶端、用戶綁定的,這表示某一用戶授予某一個客戶端訪問資源的權限。

我們接下來實現的授權服務器主要包含以下模塊,如圖 11-5 所示:

微服務統一認證與授權的 Go 語言實現

  • ClientDetailsService,用於提供獲取客戶端信息;
  • UserDetailsService,用於獲取用戶信息;
  • TokenGrant,用於根據授權類型進行不同的驗證流程,並使用 TokenService 生成訪問令牌;
  • TokenService,生成並管理令牌,使用 TokenStore 存儲令牌;
  • TokenStore,負責令牌的存儲工作。

鑑於篇幅所限,我們的授權服務器僅提供密碼類型獲取訪問令牌,但是提供了簡便的可擴展的機制,讀者可以根據自己的需要進行擴展實現。

3.2 用戶服務和客戶端服務

用戶服務和客戶端服務的作用類型,都是根據對應的唯一標識加載用戶和客戶端信息,用於接下來的用戶信息和客戶端信息的校驗。我們定義的用戶信息和客戶端信息結構體如下:


微服務統一認證與授權的 Go 語言實現


除了它們具備的基本信息,還提供了 #IsMatch 方法用於驗證賬號信息和密碼是否匹配的 方法。由於我們的信息都是明文存儲的,所以直接比較信息是否相等即可,也可以根據項目的需求,在其中使用一些加密算法,避免敏感信息明文存儲。

UserDetailsService 和 ClientDetailService 服務都僅提供一個方法,用於根據對應的標識加載信息,接口定義如下所示:


微服務統一認證與授權的 Go 語言實現


用戶信息和客戶端信息可以來源多處,我們可以從數據庫中、緩存中甚至通過 RPC 的方式從其他用戶微服務中加載。

小結

本文主要介紹了微服務架構中的統一認證與授權相關概念,以及授權服務器實現涉及到的結構體和服務接口。TokenGrant 令牌生成器和 TokenService 令牌服務以及其他的實現將會在下篇介紹。

微服務合集


分享到:


相關文章: