微服务统一认证与授权的 Go 语言实现
各位讀者朋友鼠年大吉,祝各位新的一年身體健康,萬事如意!
最近疫情嚴重,是一個特殊時期,大家一定要注意防護。很多省份推遲了企業開工的時間,大部分的互聯網公司也都是下周開始遠程辦公。大家可以利用在家的幾天時間學習充電,反正也出不去(🙂🙂🙂)。
今天筆者要寫得是 Go 微服務相關的組件實踐,筆者在好幾年前就接觸 Go 語言,去年開始從事 Go 微服務相關的開發,在過程中也和小伙伴聯合編寫了一本 《Go 高并發與微服務實戰》書籍,即將出版上市。本文是截取其中的搶先版閱覽,介紹微服務統一認證與授權的 Go 語言實現。
1 前言
統一認證與授權是微服務架構的基礎功能,微服務架構不同于單體應用的架構,認證和授權非常集中。當服務拆分之后,對各個微服務認證與授權變得非常分散,所以在微服務架構中,將集成統一認證與授權的功能,作為橫切關注點。
2 常見的認證與授權方案
常見的認證與授權方案有 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 授權服務器,對資源所有者的授權進行認證,成功后向客戶端發送訪問令牌。
在很多時候,資源服務器和授權服務器是合二為一的,在授權交互的時候是授權服務器,在請求資源交互是資源服務器。但是授權服務器是單獨的實體,它可以發出被多個資源服務器接受的訪問令牌。
協議流程
首先看一張來自官方提供的流程圖:
+--------+ +---------------+| |--(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 授權碼類型
- 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)密碼類型
密碼類型(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)向授權服務器獲取新的訪問令牌。
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 令牌服務以及其他的實現將會在下篇介紹。
推薦閱讀
微服務合集
訂閱最新文章,歡迎關注我的公眾號
總結
以上是生活随笔為你收集整理的微服务统一认证与授权的 Go 语言实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ORACLE 10g下载地址
- 下一篇: 设计 色彩 构图 创意_我们可以从时尚的