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

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

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

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

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

1 前言

統一認證與受權是微服務架構的基礎功能,微服務架構不一樣於單體應用的架構,認證和受權很是集中。當服務拆分以後,對各個微服務認證與受權變得很是分散,因此在微服務架構中,將集成統一認證與受權的功能,做爲橫切關注點。web

2 常見的認證與受權方案

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

2.1 OAuth

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

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

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 受權服務器,對資源全部者的受權進行認證,成功後向客戶端發送訪問令牌。

在不少時候,資源服務器和受權服務器是合二爲一的,在受權交互的時候是受權服務器,在請求資源交互是資源服務器。可是受權服務器是單獨的實體,它能夠發出被多個資源服務器接受的訪問令牌。

協議流程

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

+--------+                               +---------------+
 |        |--(1)- Authorization Request ->|   Resource    |
 |        |                               |     Owner     |
 |        |<-(2)-- Authorization Grant ---|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(3)-- Authorization Grant -->| Authorization |
 | Client |                               |     Server    |
 |        |<-(4)----- Access Token -------|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(5)----- Access Token ------>|    Resource   |
 |        |                               |     Server    |
 |        |<-(6)--- Protected Resource ---|               |
 +--------+                               +---------------+
複製代碼

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

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

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

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

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

受權碼類型

受權碼類型(authorization code)經過重定向的方式讓資源全部者直接與受權服務器進行交互來進行受權,避免了資源全部者信息泄漏給客戶端,是功能最完整、流程最嚴密的受權類型,可是須要客戶端必須能與資源全部者的代理(一般是Web瀏覽器)進行交互,和可從受權服務器中接受請求(重定向給予受權碼),受權流程以下:

+----------+
 | Resource |
 |   Owner  |
 |          |
 +----------+
      ^
      |
     (2)
 +----|-----+          Client Identifier      +---------------+
 |         -+----(1)-- & Redirection URI ---->|               |
 |  User-   |                                 | Authorization |
 |  Agent  -+----(2)-- User authenticates --->|     Server    |
 |          |                                 |               |
 |         -+----(3)-- Authorization Code ---<|               |
 +-|----|---+                                 +---------------+
   |    |                                         ^      v
  (1)  (3)                                        |      |
   |    |                                         |      |
   ^    v                                         |      |
 +---------+                                      |      |
 |         |>---(4)-- Authorization Code ---------'      |
 |  Client |          & Redirection URI                  |
 |         |                                             |
 |         |<---(5)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)
複製代碼
  1. 客戶端引導資源全部者的用戶代理到受權服務器的endpoint,通常經過重定向的方式。客戶端提交的信息應包含客戶端標識(client identifier)、請求範圍(requested scope)、本地狀態(local state)和用於返回受權碼的重定向地址(redirection URI);
  2. 受權服務器認證資源全部者(經過用戶代理),並確認資源全部者容許仍是拒絕客戶端的訪問請求;
  3. 若是資源全部者授予客戶端訪問權限,受權服務器經過重定向用戶代理的方式回調客戶端提供的重定向地址,並在重定向地址中添加受權碼和客戶端先前提供的任何本地狀態;
  4. 客戶端攜帶上一步得到的受權碼向受權服務器請求訪問令牌。在這一步中受權碼和客戶端都要被受權服務器進行認證。客戶端須要提交用於獲取受權碼的重定向地址;
  5. 受權服務器對客戶端進行身份驗證,和認證受權碼,確保接收到的重定向地址與第三步中用於的獲取受權碼的重定向地址相匹配。若是有效,返回訪問令牌,以及可能返回的刷新令牌(Refresh Token)。

密碼類型

密碼類型(resource owner password credentials)須要資源全部者將密碼憑證交予客戶端,客戶端經過本身持有的信息直接向受權服務器獲取受權。在這種狀況下,須要資源全部者對客戶端高度可信任,同時客戶端不容許保存密碼憑證。這種受權類型適用於可以獲取資源全部者的憑證(credentials)(如用戶名和密碼)的客戶端。受權流程以下:

+----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      v
      |    Resource Owner
     (1) Password Credentials
      |
      v
 +---------+                                  +---------------+
 |         |>--(2)---- Resource Owner ------->|               |
 |         |         Password Credentials     | Authorization |
 | Client  |                                  |     Server    |
 |         |<--(3)---- Access Token ---------<|               |
 |         |    (w/ Optional Refresh Token)   |               |
 +---------+                                  +---------------+
複製代碼
  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。

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

{
	"alg": "HS256"
	"typ": "JWT"
}
複製代碼

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Playload

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

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

通常不建議在payload中添加任何的敏感信息,由於Base64是對稱解密的,這意味着payload中的信息的是可見的。

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

{
 	"name": "cang wu",
 	"exp": 1518051157,
 	"userId": "123456"
}
複製代碼

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

eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0

Signature

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

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
複製代碼

用到的加密算法爲HMACSHA256

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

一個簡單的簽名以下:

IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs

這將成爲JWT的第三部分。

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0.IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs

3 受權服務器

3.1 總體架構

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

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

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

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

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

通常來說,每個客戶端均可覺得用戶申請訪問令牌,所以一個有效的訪問令牌是和客戶端、用戶綁定的,這表示某一用戶授予某一個客戶端訪問資源的權限。

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

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

鑑於篇幅所限,咱們的受權服務器僅提供密碼類型獲取訪問令牌,可是提供了簡便的可擴展的機制,讀者能夠根據本身的須要進行擴展實現。

3.2 用戶服務和客戶端服務

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

type UserDetails struct {
	// 用戶標識
	UserId int
	// 用戶名 惟一
	Username string
	// 用戶密碼
	Password string
	// 用戶具備的權限
	Authorities []string
}
// 驗證用戶名和密碼是否匹配
func (userDetails *UserDetails)IsMatch(username string, password string) bool {
	return userDetails.Password == password && userDetails.Username == username
}


type ClientDetails struct {
	// client 的標識
	ClientId string
	// client 的密鑰
	ClientSecret string
	// 訪問令牌有效時間,秒
	AccessTokenValiditySeconds int
	// 刷新令牌有效時間,秒
	RefreshTokenValiditySeconds int
	// 重定向地址,受權碼類型中使用
	RegisteredRedirectUri string
	// 可使用的受權類型
	AuthorizedGrantTypes []string
}

// 驗證 clientId 和 ClientSecret 是否匹配
func (clientDetails *ClientDetails) IsMatch(clientId string, clientSecret string) bool {
	return clientId == clientDetails.ClientId && clientSecret == clientDetails.ClientSecret
}
複製代碼

除了它們具有的基本信息,還提供了 #IsMatch 方法用於驗證帳號信息和密碼是否匹配的 方法。因爲咱們的信息都是明文存儲的,因此直接比較信息是否相等便可,也能夠根據項目的需求,在其中使用一些加密算法,避免敏感信息明文存儲。

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

type UserDetailsService interface {
	// 根據用戶名加載用戶信息
	GetUserDetailByUsername(username string)(*UserDetails, error)
}

type ClientDetailService interface {
	// 根據 clientId 加載客戶端信息
	GetClientDetailByClientId(clientId string) (*ClientDetails, error)
}
複製代碼

用戶信息和客戶端信息能夠來源多處,咱們能夠從數據庫中、緩存中甚至經過 RPC 的方式從其餘用戶微服務中加載。

小結

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

推薦閱讀

微服務合集

訂閱最新文章,歡迎關注個人公衆號

微信公衆號
相關文章
相關標籤/搜索