各位讀者朋友鼠年大吉,祝各位新的一年身體健康,萬事如意!javascript
最近疫情嚴重,是一個特殊時期,你們必定要注意防禦。不少省份推遲了企業開工的時間,大部分的互聯網公司也都是下週開始遠程辦公。你們能夠利用在家的幾天時間學習充電,反正也出不去(🙂🙂🙂)。html
今天筆者要寫得是 Go 微服務相關的組件實踐,筆者在好幾年前就接觸 Go 語言,去年開始從事 Go 微服務相關的開發,在過程當中也和小夥伴聯合編寫了一本 《Go 高併發與微服務實戰》書籍,即將出版上市。本文是截取其中的搶先版閱覽,介紹微服務統一認證與受權的 Go 語言實現。java
統一認證與受權是微服務架構的基礎功能,微服務架構不一樣於單體應用的架構,認證和受權很是集中。當服務拆分以後,對各個微服務認證與受權變得很是分散,因此在微服務架構中,將集成統一認證與受權的功能,做爲橫切關注點。web
常見的認證與受權方案有 OAuth、分佈式 Session、OpenID 和 JWT 等,下面咱們將分別介紹這四種方案。算法
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種角色
在不少時候,資源服務器和受權服務器是合二爲一的,在受權交互的時候是受權服務器,在請求資源交互是資源服務器。可是受權服務器是單獨的實體,它能夠發出被多個資源服務器接受的訪問令牌。
協議流程
首先看一張來自官方提供的流程圖:
+--------+ +---------------+
| |--(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個步驟:
爲了獲取訪問令牌,客戶端必須獲取到資源全部者的受權許可。OAuth2默認定了四種受權類型,固然也提供了用於定義額外的受權類型的擴展機制。默認的四種受權類型爲:
下面對經常使用的受權碼類型和密碼類型進行詳細的介紹。
受權碼類型
受權碼類型(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)
複製代碼
密碼類型
密碼類型(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) | |
+---------+ +---------------+
複製代碼
令牌刷新
客戶端從受權服務器中獲取的訪問令牌(access token)通常是具有失效性的,在訪問令牌過時的狀況下,持有有效用戶憑證的客戶端能夠再次向受權服務器請求訪問令牌,可是若是不持有用戶憑證的客戶端能夠經過和上次訪問令牌一同返回的刷新令牌(refresh token)向受權服務器獲取新的訪問令牌。
HTTP 協議是無狀態的協議。一旦數據交換完畢,客戶端與服務器端的鏈接就會關閉,再次交換數據須要創建新的鏈接。這就意味着服務器沒法從鏈接上跟蹤會話。
會話,指用戶登陸網站後的一系列動做,好比瀏覽商品添加到購物車併購買。會話(Session)跟蹤是 Web 程序中經常使用的技術,用來跟蹤用戶的整個會話。經常使用的會話跟蹤技術是 Cookie 與 Session。
Cookie 其實是一小段的文本信息。客戶端請求服務器,若是服務器須要記錄該用戶狀態,就使用 response 向客戶端瀏覽器頒發一個 Cookie。客戶端會把 Cookie 保存起來。
當瀏覽器再請求該網站時,瀏覽器把請求的網址連同該 Cookie 一同提交給服務器。服務器檢查該 Cookie,以此來辨認用戶狀態。服務器還能夠根據須要修改 Cookie 的內容。
Session 是另外一種記錄客戶狀態的機制,不一樣的是 Cookie 保存在客戶端瀏覽器中,而 Session 保存在服務器上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄
在服務器上。這就是 Session。客戶端瀏覽器再次訪問時只須要從該 Session 中查找該客戶的狀態就能夠了。
每一個用戶訪問服務器都會創建一個 session,那服務器是怎麼標識用戶的惟一身份呢?事實上,用戶與服務器創建鏈接的同時,服務器會自動爲其分配一個 SessionId。
簡單來講,Cookie 經過在客戶端記錄信息肯定用戶身份,Session經過在服務器端記錄信息肯定用戶身份。
某些站點看到容許以 OpenID 的方式登錄,如使用 Facebook 帳號或者 Google 帳號登錄站點。
OpenID 和 OAuth 很像。但本質上來講它們是大相徑庭的兩個東西:
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
頭部一般由兩部分組成:
一個簡單的頭部例子以下:
{
"alg": "HS256"
"typ": "JWT"
}
複製代碼
而後這部分JSON會被Base64Url編碼用於構成JWT的第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Playload
有效負載是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
通過以上的簡單介紹,咱們已經瞭解了目前常見的統一認證與鑑權的方案,接下來咱們將基於 OAuth2 協議和 JWT 實現一套簡單的認證和受權系統。系統主要由兩個服務組成,受權服務器和資源服務器,它們之間的交互圖 11-4 所示:
客戶端想要訪問資源服務器中用戶持有的資源信息,首先須要攜帶用戶憑證向受權服務器請求訪問令牌。受權服務器在驗證過客戶端和用戶憑證的有效性後,它將返回生成的訪問令牌給客戶端。接着客戶端攜帶訪問令牌向資源服務器請求對應的用戶資源,在資源服務器經過受權服務器驗證過訪問令牌有效後,將返回對應的用戶資源。
不少時候,受權服務器和資源服務器是合二爲一,便可以頒發訪問令牌,也對用戶資源受限訪問;也能夠將它們的職責劃分得更加詳細,受權服務器主要負責令牌的頒發和令牌的驗證,而資源服務器負責對用戶資源進行保護,僅容許持有有效訪問令牌的請求訪問受限資源。
受權服務器的主要職責有頒發訪問令牌和驗證訪問令牌,對此咱們須要對外提供兩個接口:
通常來說,每個客戶端均可覺得用戶申請訪問令牌,所以一個有效的訪問令牌是和客戶端、用戶綁定的,這表示某一用戶授予某一個客戶端訪問資源的權限。
咱們接下來實現的受權服務器主要包含如下模塊,如圖 11-5 所示:
鑑於篇幅所限,咱們的受權服務器僅提供密碼類型獲取訪問令牌,可是提供了簡便的可擴展的機制,讀者能夠根據本身的須要進行擴展實現。
用戶服務和客戶端服務的做用類型,都是根據對應的惟一標識加載用戶和客戶端信息,用於接下來的用戶信息和客戶端信息的校驗。咱們定義的用戶信息和客戶端信息結構體以下:
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 令牌服務以及其餘的實現將會在下篇介紹。