1. 程式人生 > >使用微服務架構思想,設計部署API代理閘道器和OAuth2.0授權認證框架

使用微服務架構思想,設計部署API代理閘道器和OAuth2.0授權認證框架

1,授權認證與微服務架構

1.1,由不同團隊合作引發的授權認證問題

去年的時候,公司開發一款新產品,但人手不夠,將B/S系統的Web開發外包,外包團隊使用Vue.js框架,呼叫我們的WebAPI,但是這些WebAPI並不在一臺伺服器上,甚至可能是第三方提供的WebAPI。同時處於系統安全的架構設計,後端WebAPI是不能直接暴露在外面的;另一方面,我們這個新產品還有一個C/S系統,C端登入的時候,要求統一到B/S端登入,可以從C端無障礙的訪問任意B/S端的頁面,也可以呼叫B/S系統的一些API,所以又增加了一個API閘道器代理。

整個系統的架構示意圖如下:

注:上圖還有一個iMSF,這是一個實時訊息服務框架,這裡用來做檔案服務,參見《

訊息服務框架使用案例之--大檔案上傳(斷點續傳)功能》。在Web端會讀取這些上傳的檔案。

1.2,微服務--分散式“最徹底”的分

1.2.1,為什麼需要分散式

大部分情況下,如果你的系統不是很複雜,API和授權認證服務,檔案服務都可以放到一臺伺服器:Web Port 伺服器上,但要把它們分開部署到不同的站點,或者不同的伺服器,主要是出於以下考慮:

1,職責單一:每一個服務都只做一類工作,比如某某業務WebAPI,授權服務,使用者身份認證服務,檔案服務等;職責單一使得開發、部署和維護變得容易,比如很容易知道當前是授權服務的問題,而不是業務API問題。

2,系統安全:採用內外網隔離的方案,一些功能需要直接暴露在公網,這需要付出額外的成本,比如頻寬租用和安全設施;另外一些功能部署在內網,這樣能夠提供更大的安全保證。

3,易於維護:每一個服務職責都比較單一,所以每一個服務都足夠小,那麼開發維護就更容易,比如要更新一個功能,只需要更新一個服務而不用所有伺服器都暫停;另一方面也更加容易監控伺服器的負載,如果發現某一個伺服器負載太大可以增加伺服器來分散負載。

4,第三方接入:現在系統越來越複雜,內部的系統很可能需要跟第三方的系統對接,一起協同工作;或者整個系統一部分是 .NET開發的,一部分又是Java平臺開發的,兩個平臺部署的環境有很大差異,沒法部署在一起;或者雖然同是ASP.NET MVC,但是一個是MVC3,一個是MVC5,所以需要分別獨立部署。

以上就是各個服務需要分開部署的原因,而這樣做的結果就是我們常說的分散式計算了,這是自然需求的結果,不是為了分而才分。

1.2.2,依賴於中間層而不直接依賴於服務

客戶端直接訪問後端服務,對後端的服務會形成比較強的依賴。有架構經驗的朋友都知道,解決依賴的常見手段就是新增一箇中間層,客戶端依賴於這個中間層而不是直接依賴於服務層。這樣做有幾個很大的好處:

  • 當服務負載過大的時候可以在中間層做負載均衡;
  • 或者後端某個服務出現問題可以切換主備服務;
  • 或者替換後端某個服務的版本做灰度釋出。

另一方面,當後端服務部署為多個獨立的程序/伺服器後,客戶端直接訪問這些服務,將是一個更加較複雜的問題,負載均衡,主備切換,灰度釋出等運維功能更難操作,除此之外,還有下面兩個比較重要的問題:

  • 客戶端直接訪問後端多個服務,將暴露過多的後端伺服器地址,從而增加安全隱患;
  • 後端服務太多,需要在客戶端維護這些服務訪問關係,增加開發除錯的複雜性;
  • B/S頁面的AJax跨域問題,WebAPI地址跟主站地址不一樣,要解決跨域問題比較複雜並且也會增加安全隱患。

所以,為了解決客戶端對後端服務層的依賴,並且解決後端服務太多以後引起的問題,我們需要在客戶端和後端服務層之間新增一箇中間層,這個中間層就是我們的服務代理層,也就是我們後面說的服務閘道器代理(WebAPI Gateway Proxy),它作為我們所有Web訪問的入口站點,這就是上圖所示的 Web Port。有了閘道器代理,後臺所有的WebAPI都可以通過這個統一的入口提供對外服務的功能,而對於後端不同服務地址的路由,由閘道器代理的路由功能來實現,所以這個代理功能很像Nginx這樣的反向代理,只不過,這裡僅僅代理WebAPI,而不是其它Web資源。

現在,閘道器已經成為很多分散式系統的標配,比如TX的這個架構:

注:上圖來源於網路,侵刪!

另外,這個讀寫分離代理,如果使用SOD框架,可以在AdoHelper物件直接設定讀寫不同的連線字串簡單達到效果。

1.2.3,微服務架構

經過上面的設計,我們發現這個架構有幾個特點:

  1. 每個服務足夠小,職責單一;
  2. 每個服務執行在自己的程序或者獨立的伺服器中,獨立釋出部署和開發維護;
  3. 服務對外提供訪問或者服務之間進行通訊,都是使用輕量級的HTTP API;
  4. 每個服務有自己獨立的儲存,彼此之間進行資料互動都通過介面進行;
  5. 有一個API代理閘道器統一提供服務的對外訪問。

這些特點是非常符合現在流行的微服務思想的,比如在《什麼是微服務》這篇文章中,像下面說的這樣:

微服務最早由Martin Fowler與James Lewis於2014年共同提出,微服務架構風格是一種使用一套小服務來開發單個應用的方式途徑,每個服務執行在自己的程序中,
並使用輕量級機制通訊,通常是HTTP API,這些服務基於業務能力構建,並能夠通過自動化部署機制來獨立部署,這些服務使用不同的程式語言實現,以及不同資料儲存技術,
並保持最低限度的集中式管理。

所以我們這個架構是基本符合微服務思想的,它的誕生背景也是要解決其它傳統單體軟體專案現在遇到的問題一樣的,是在比較複雜的實際需求環境下自然而然的一種需求,不過好在它沒有過多的“技術債務”,所以設計實施起來比較容易。下面我們來詳細看看這個架構是如何落地的。

2,“授權\認證\資源”獨立服務的OAuth2.0架構

2.1,為什麼需要OAuth2.0 ?

OAuth 2.0已經是一個“使用者驗證和授權”的工業級標準。OAuth(開放授權)是一個開放標準,1.0版本於2006年創立,它允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。 OAuth 2.0關注客戶端開發者的簡易性,同時為Web應用,桌面應用和手機,和起居室裝置提供專門的認證流程。2012年10月,OAuth 2.0協議正式釋出為RFC 6749。以上內容詳見OAuth 2.0官網

現在百度開放平臺,騰訊開放平臺等大部分的開放平臺都是使用的OAuth 2.0協議作為支撐,國內越來越多的企業都開始支援OAuth2.0協議。現在,我們的產品設計目標是要能夠和第三方系統對接,那麼在對接過程中的授權問題就是無法迴避的問題。在我們原來的產品中,有使用者授權驗證的模組,但並沒有拆分出獨立的服務,用它與第三方系統對接會導致比較大的耦合性;另一方面,與第三方系統對接合作不一定每次都是以我們為主導,也有可能要用第三方的授權認證系統。這就出現了選擇哪一方的授權認證方案的問題。之前我曾經經歷過一個專案,因為其中的授權認證問題導致系統遲遲不能整合。所以,選擇一個開放標準的授權認證方案,才是最佳的解決方案,而OAuth 2.0正是這樣的方案。

2.2,OAuth的名詞解釋和規範

(1)Third-party application:第三方應用程式,本文中又稱”客戶端”(client),即上一節例子中的“Web Port”或者C/S客戶端應用程式。
(2)HTTP service:HTTP服務提供商,即上一節例子中提供軟體產品的我們公司或者第三方公司。
(3)Resource Owner:資源所有者,本文中又稱“使用者”(user)。
(4)User Agent:使用者代理,本文中就是指瀏覽器或者C/S客戶端應用程式。
(5)Authorization server:授權伺服器,即服務提供商專門用來處理認證的伺服器。
(6)Resource server:資源伺服器,即服務提供商存放使用者生成的資源的伺服器,即上一節例子中的內部API伺服器、第三方外部API伺服器和檔案伺服器等。它與認證伺服器,可以是同一臺伺服器,也可以是不同的伺服器。

以上名詞是OAuth規範內必須理解的一些名詞,然後我們才能方便的討論OAuth2.0是如何授權的。有關OAuth的思路、執行流程和詳細的四種授權模式,請參考阮一峰老師的《理解OAuth 2.0》。

2.3,OAuth2.0的授權模式

為了表述方便,先簡單說說這4種授權模式:

  1. 授權碼模式(authorization code)--是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺伺服器,與"服務提供商"的認證伺服器進行互動。
  2. 簡化模式(implicit)--不通過第三方應用程式的伺服器,直接在瀏覽器中向認證伺服器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。
  3. 密碼模式(resource owner password credentials)--使用者向客戶端提供自己的使用者名稱和密碼。客戶端使用這些資訊,向"服務商提供商"索要授權。在這種模式中,使用者必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。
  4. 客戶端模式(client credentials)--指客戶端以自己的名義,而不是以使用者的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,使用者直接向客戶端註冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。

在我們的需求中,使用者不僅僅通過B/S系統的瀏覽器進行操作,還會通過C/S程式的客戶端進行操作,B/S,C/S系統主要都是我們提供和整合的,客戶購買了我們這個產品要使用它就意味著客戶信任我們的產品。授權碼模式雖然是最完整的授權模式,但是授權碼模式授權完成後需要瀏覽器的跳轉,顯然瀏覽器無法直接跳轉到我們的C/S客戶端,雖然從技術上可以模擬,但實現起來成本還是比較高;簡化模式也有這個問題。所以我們最終決定採用OAuth2.0的密碼模式。

2.4,OAuth2.0密碼模式授權流程

 簡單來說,密碼模式的步驟如下:

  1.  使用者向客戶端提供使用者名稱和密碼。
  2. 客戶端將使用者名稱和密碼發給認證伺服器,向後者請求令牌。
  3. 認證伺服器確認無誤後,向客戶端提供訪問令牌。

 上面這個步驟只是說明了令牌的獲取過程,也就是我們常說使用者登陸成功的過程。當用戶登陸成功之後,客戶端得到了一個訪問令牌,然後再使用這個令牌去訪問資源伺服器,具體說來還有如下後續過程:

  • 4,客戶端攜帶此訪問令牌,訪問資源伺服器;
  • 5,資源伺服器去授權伺服器驗證客戶端的訪問令牌是否有效;
  • 6,如果訪問令牌有效,授權伺服器給資源伺服器傳送使用者標識資訊;
  • 7,資源伺服器根據使用者標識資訊,處理業務請求,最後傳送響應結果給客戶端。

下面是流程圖:

注意:這個流程適用於資源伺服器、授權伺服器相分離的情況,否則,流程中的第5,6步不是必須的,甚至第4,7步都是顯而易見的事情而不必說明。現在大部分有關OAuth2.0的介紹文章都沒有4,5,6,7步驟的說明,可能為了表述方便,預設都是將授權伺服器跟資源伺服器合在一起部署的。

2.5,授權、認證與資源服務的分離

什麼情況下授權伺服器跟資源伺服器必須分開呢?

如果一個系統有多個資源伺服器並且這些資源伺服器的框架版本不相容,執行環境有差異,程式碼平臺不同(比如一個是.NET,一個是Java),或者一個是內部系統,一個是外部的第三方系統,必須分開部署。在這些情況下,授權伺服器跟任意一個資源伺服器部署在一起都不利於另一些資源伺服器的使用,導致系統整合成本增加。這個時候,授權伺服器必須跟資源伺服器分開部署,我們在具體實現OAuth2.0系統的時候,需要做更多的事情。

什麼情況下授權伺服器跟認證伺服器必須分開呢?

 授權(authorization)和認證(authentication)有相似之處,但也是兩個不同的概念:

  • 授權(authorization):授權,批准;批准(或授權)的證書;
  • 認證(authentication):認證;身份驗證;證明,鑑定;密押。

僅僅從這兩個詞的名詞定義可能不太容易分辨,我們用實際的例子來說明他們的區別:

有一個管理系統,包括成熟的人員管理,角色管理,許可權管理,系統登入的時候,使用者輸入的使用者名稱和密碼到系統的人員資訊表中查詢,通過後取得該使用者的角色許可權。

在這個場景中,使用者登入系統實際上分為了3個步驟:

  1. 使用者在登入介面,輸入使用者名稱和密碼,提交登入請求;
  2. 【認證】系統校驗使用者輸入的使用者名稱和密碼是否在人員資訊表中;
  3. 【授權】給當前使用者授予相應的角色許可權。

現在,該管理系統需要和第三方系統對接,根據前面的分析,這種情況下最好將授權功能獨立出來,採用OAuth這種開放授權方案,而認證問題,原有管理系統堅持使用者資訊是敏感資訊,不能隨意洩露給第三方,要求在原來管理系統完成認證。這樣一來,授權和認證,只好分別作為兩個服務,獨立部署實現了。

本文的重點就是講述如何在授權伺服器和資源伺服器相分離,甚至授權和認證伺服器相分離的情況下,如何設計實現OAuth2.0的問題。

3,PWMIS OAuth2.0 方案

PWMIS OAuth2.0 方案就是一個符合上面要求的授權與認證相分離,授權與資源服務相分離的架構設計方案,該方案已經成功支撐了我們產品的應用。下面分別來說說該方案是如何設計和落地的。

3.1,使用Owin中介軟體搭建OAuth2.0認證授權伺服器

這裡主要總結下本人在這個產品中搭建OAuth2.0伺服器工作的經驗。至於為何需要OAuth2.0、為何是Owin、什麼是Owin等問題,不再贅述。我假定讀者是使用Asp.Net,並需要搭建OAuth2.0伺服器,對於涉及的Asp.Net Identity(Claims Based Authentication)、Owin、OAuth2.0等知識點已有基本瞭解。若不瞭解,請先參考以下文章:

我們的工作,可以從研究《OWIN OAuth 2.0 Authorization Server》這個DEMO開始,不過為了更好的結合本文的主題,實現授權與認證相分離的微服務架構,推薦大家直接從我的DEMO開始:

PS:大家覺得好,先點個贊支援下,謝謝!

克隆我這個DEMO到本地,下面開始我們OAuth2.0如何落地的正式講解。

3.2,PWMIS.OAuth2.0解決方案介紹

首先看到解決方案檢視,先逐個做下簡單說明:

編號

角色

程式集名稱

說明

1

授權伺服器

PWMIS.OAuth2.AuthorizationCenter

授權中心

ASP.NET Web API+OWIN

2

資源伺服器

Demo.OAuth2.WebApi

提供API資源

ASP.NET Web API+OWIN

Demo.OAuth2.WebApi2

 提供API資源

 ASP.NET Web API 

3

客戶端

Demo.OAuth2.ConsoleTest

控制檯測試程式,測試令牌申請等功能

 Demo.OAuth2.WinFormTest

 測試登入到B/S和開啟B/S頁面等功能

4

 API代理閘道器

Demo.OAuth2.Port

使用者的Web入口,本測試程式入口

ASP.NET MVC 5.0

5

認證伺服器

Demo.OAuth2.IdentityServer

簡單登入賬號認證

ASP.NET Web API

Demo.OAuth2.Mvc

 簡單登入賬號認證,支援登入會話

 ASP.NET Web MVC 

6

 其它

PWMIS.OAuth2.Tools

提供OAuth2.0 協議訪問的一些有用的工具類

3.2.1,執行解決方案

將解決方案的專案,除了PWMIS.OAuth2.Tools,全部設定為啟動專案,啟動之後,在 http://localhost:62424/ 站點,輸入下面的地址:

http://localhost:62424/Home

然後就可以看到下面的介面:

點選登入頁面,為了方便演示,不真正驗證使用者名稱和密碼,所以隨意輸入,提交後結果如下圖:

點選確定,進入了業務操作頁面,如下圖:

如果能夠看到這個頁面,我們的OAuth2.0演示程式就成功了。

還可以執行解決方案裡面的WinForm測試程式,先登入,然後執行效能測試,如下圖:

更多資訊,請參考下文的【3.8整合C/S客戶端訪問】

下面我們來看看各個程式集專案的構建過程。

3.3,專案 PWMIS.OAuth2.AuthorizationCenter

首先新增一個MVC5專案PWMIS.OAuth2.AuthorizationCenter,然後新增如下包引用:

Microsoft.AspNet.Mvc
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.OAuth
Microsoft.Owin.Security.Cookies

然後在專案根目錄下新增一個OWin的啟動類 Startup:

using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Web.Http;

namespace PWMIS.OAuth2.AuthorizationCenter
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            var OAuthOptions = new OAuthAuthorizationServerOptions
            {
                AllowInsecureHttp = true,
                AuthenticationMode = AuthenticationMode.Active,
                TokenEndpointPath = new PathString("/api/token"), //獲取 access_token 授權服務請求地址
                AuthorizeEndpointPath = new PathString("/authorize"), //獲取 authorization_code 授權服務請求地址
                AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(60), //access_token 過期時間,預設10秒太短

                Provider = new OpenAuthorizationServerProvider(), //access_token 相關授權服務
                AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(), //authorization_code 授權服務
                RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授權服務
            };
            app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
        }

        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
            ConfigureAuth(app);

            var configuration = new HttpConfiguration();
            WebApiConfig.Register(configuration);
            app.UseWebApi(configuration);

         }

      }
}

上面的程式碼中,定義了access_token 授權服務請求地址和access_token 過期時間,這裡設定60秒後過期。由於本篇著重講述OAuth2.0的密碼授權模式,我們直接看到類 OpenAuthorizationServerProvider的定義:

 public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        /// <summary>
        /// 驗證 client 資訊
        /// </summary>
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            string clientId;
            string clientSecret;
            if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
            {
                context.TryGetFormCredentials(out clientId, out clientSecret);
            }
            if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
            {
                context.SetError("PWMIS.OAuth2 invalid_client", "client or clientSecret is null or empty");
                return;
            }

            var identityRepository = IdentityRepositoryFactory.CreateInstance();
            try
            {
                if (!await identityRepository.ValidateClient(clientId, clientSecret))
                {
                    context.SetError("PWMIS.OAuth2 invalid_client", "client or clientSecret is not valid");
                    return;
                }
            }
            catch (Exception ex)
            {
                context.SetError("PWMIS.OAuth2 identity_repository_error", ex.Message );
                Log("PWMIS.OAuth2 identity_repository_error:" + ex.Message);
                return;
            }
          
            context.Validated();
        }

        /// <summary>
        /// 生成 access_token(resource owner password credentials 授權方式)
        /// </summary>
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            string validationCode = "";
            string sessionId = "";
            if (string.IsNullOrEmpty(context.UserName))
            {
                context.SetError("PWMIS.OAuth2 invalid_username", "username is not valid");
                return;
            }
            if (string.IsNullOrEmpty(context.Password))
            {
                context.SetError("PWMIS.OAuth2 invalid_password", "password is not valid");
                return;
            }
            if (context.Scope.Count > 0)
            {
                //處理使用者會話標識和驗證碼
                var temp= context.Scope.FirstOrDefault(p => p.Contains("ValidationCode:"));
                if (temp != null)
                {
                    validationCode = temp.Split(':')[1];
                }

                var temp1 = context.Scope.FirstOrDefault(p => p.Contains("SessionID:"));
                if (temp1 != null)
                {
                    sessionId = temp1.Split(':')[1];
                }
            }

            IdentityService service = new IdentityService();
            try
            {
                LoginResultModel user = await service.UserLogin(context.UserName, context.Password,sessionId, validationCode);
                if (user == null)
                {
                    context.SetError("PWMIS.OAuth2 invalid_identity", "username or password is not valid");
                    return;
                }
                else  if (string.IsNullOrEmpty(user.UserName))
                {
                    context.SetError("PWMIS.OAuth2 invalid_identity", user.ErrorMessage);
                    return;
                }
            }
            catch (Exception ex)
            {
                context.SetError("PWMIS.OAuth2 identity_service_error", ex.Message );
                Log("PWMIS.OAuth2 identity_service_error:" + ex.Message);
                return;
            }
           

            var OAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
            OAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
            context.Validated(OAuthIdentity);
        }

        /// <summary>
        /// 驗證 access_token 的請求
        /// </summary>
        public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
        {
            if (context.TokenRequest.IsAuthorizationCodeGrantType || 
                context.TokenRequest.IsRefreshTokenGrantType || 
                context.TokenRequest.IsResourceOwnerPasswordCredentialsGrantType ||
                context.TokenRequest.IsClientCredentialsGrantType)
            {
                context.Validated();
            }
            else
            {
                context.Rejected();
            }
        }
          
    }
}
OpenAuthorizationServerProvider

 token過期時間不宜太長,比如一天,這樣不安全,但也不能太短,比如10秒,這樣當API訪問量比較大的時候會增大重新整理token的負擔,所以這裡設定成60秒。

3.3.1,驗證客戶端資訊

在本類的第一個方法 ValidateClientAuthentication 驗證客戶端的資訊,這裡的客戶端可能是C/S程式的客戶端,也可能是訪問授權伺服器的閘道器代理伺服器,OAuth2.0會驗證需要生成訪問令牌的客戶端,只有合法的客戶端才可以提供後續的生成令牌服務。

客戶端資訊有2個部分,一個是clientId,一個是clientSecret,前者是客戶端的唯一標識,後者是授權伺服器頒發給客戶端的祕鑰,這個祕鑰可以設定有效期或者設定授權範圍。為簡便起見,我們的演示程式僅僅到資料庫去檢查下傳遞的這兩個引數是否有對應的資料記錄,使用下面一行程式碼:

 var identityRepository = IdentityRepositoryFactory.CreateInstance();

這裡會用到一個驗證客戶端的介面,包括驗證使用者名稱和密碼的方法一起定義了:

 /// <summary>
    /// 身份認證持久化介面
    /// </summary>
    public interface IIdentityRepository
    {
        /// <summary>
        /// 客戶ID是否存在
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        Task<bool> ExistsClientId(string clientId);
        /// <summary>
        /// 校驗客戶標識
        /// </summary>
        /// <param name="clientId">客戶ID</param>
        /// <param name="clientSecret">客戶祕鑰</param>
        /// <returns></returns>
        Task<bool> ValidateClient(string clientId, string clientSecret);
        /// <summary>
        /// 校驗使用者名稱密碼
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        Task<bool> ValidatedUserPassword(string userName, string password);
    }

這樣我們就可以通過反射或者簡單 IOC框架將客戶端驗證的具體實現類注入到程式中,本例實現了一個簡單的客戶端和使用者認證類,採用的是SOD框架訪問資料庫:

namespace PWMIS.OAuth2.AuthorizationCenter.Repository
{
    public class SimpleIdentityRepository : IIdentityRepository
    {
        private static System.Collections.Concurrent.ConcurrentDictionary<string, string> dictClient = new System.Collections.Concurrent.ConcurrentDictionary<string, string>();
        public async Task<bool> ExistsClientId(string clientId)
        {
            return await Task.Run<bool>(() =>
            {
                AuthClientInfoEntity entity = new AuthClientInfoEntity();
                entity.ClientId = clientId;

                OQL q = OQL.From(entity)
                    .Select(entity.ClientId)
                    .Where(entity.ClientId)
                    .END;
                AuthDbContext context = new AuthDbContext();
                AuthClientInfoEntity dbEntity = context.QueryObject<AuthClientInfoEntity>(q);
                return dbEntity != null;
            });
        }

        public async Task<bool> ValidateClient(string clientId, string clientSecret)
        {
            string dict_clientSecret;
            if (dictClient.TryGetValue(clientId, out dict_clientSecret) && dict_clientSecret== clientSecret)
            {
                return true;
            }
            else
            {
                return await Task.Run<bool>(() => {
                    AuthClientInfoEntity entity = new AuthClientInfoEntity();
                    entity.ClientId = clientId;
                    entity.ClientSecret = clientSecret;
                    OQL q = OQL.From(entity)
                        .Select(entity.ClientId)
                        .Where(entity.ClientId, entity.ClientSecret)
                        .END;
                    AuthDbContext context = new AuthDbContext();
                    AuthClientInfoEntity dbEntity = context.QueryObject<AuthClientInfoEntity>(q);
                    if (dbEntity != null)
                    {
                        dictClient.TryAdd(clientId, clientSecret);
                        return true;
                    }
                    else
                        return false;
                });
            }
            
        }

        public async Task<bool> ValidatedUserPassword(string userName, string password)
        {
            return await Task.Run<bool>(() =>
            {
                UserInfoEntity user = new UserInfoEntity();
                user.UserName = userName;
                user.Password = password;
                OQL q = OQL.From(user)
                   .Select()
                   .Where(user.UserName, user.Password)
                   .END;
                AuthDbContext context = new AuthDbContext();
                AuthClientInfoEntity dbEntity = context.QueryObject<AuthClientInfoEntity>(q);
                return dbEntity != null;
            });
        }
    }
}

AuthDbContext 類非常簡單,它會自動生成驗證客戶端所需要的表:

namespace PWMIS.OAuth2.AuthorizationCenter.Repository
{
    public class AuthDbContext:DbContext
    {
        public AuthDbContext()
            : base("OAuth2")
        {
                    
        }


        protected override bool CheckAllTableExists()
        {
            base.CheckTableExists<AuthClientInfoEntity>();
            base.CheckTableExists<UserInfoEntity>();
            return true;
        }
    }
}

3.3.2,認證使用者,生成訪問令牌

生成訪問令牌需要重寫OWIN OAuthAuthorizationServerProvider類的 GrantResourceOwnerCredentials方法(方法的詳細內容看前面【OpenAuthorizationServerProvider的定義】),方法裡面使用到了IdentityService 物件,它有一個UserLogin 方法,用來實現或者呼叫使用者認證服務: 

namespace PWMIS.OAuth2.AuthorizationCenter.Service
{
    public class IdentityService
    {
        public async Task<LoginResultModel> UserLogin(string userName, string password,string sessionId, string validationCode)
        { 
            //通過配置,決定是使用本地資料庫驗證登入,還是使用登入介面服務登入
            string identityLoginMode = System.Configuration.ConfigurationManager.AppSettings["IdentityLoginMode"];
            if (!string.IsNullOrEmpty(identityLoginMode) && identityLoginMode.ToLower() == "database")
            {
                var identityRepository = IdentityRepositoryFactory.CreateInstance();
                bool flag= await identityRepository.ValidatedUserPassword(userName, password);
                LoginResultModel result = new LoginResultModel();
                if (flag)
                {
                    result.ID = "123";
                    result.UserName = userName;
                    result.Roles = "";//暫時略
                }
                return result;
            }
            else
            {
                System.Diagnostics.Stopwatch sp = new System.Diagnostics.Stopwatch();
                var parameters = new Dictionary<string, string>();
                //parameters.Add("ID", "");
                parameters.Add("UserName", userName);
                parameters.Add("Password", password);
                parameters.Add("ID", sessionId);
                parameters.Add("ValidationCode", validationCode);
                //parameters.Add("Roles", "");

                string loginUrl = System.Configuration.ConfigurationManager.AppSettings["IdentityWebAPI"];
                string sessionCookieName = System.Configuration.ConfigurationManager.AppSettings["SessionCookieName"];
                if (string.IsNullOrEmpty(sessionCookieName))
                    sessionCookieName = "ASP.NET_SessionId";

                //新增會話標識
                CookieContainer cc = new CookieContainer();
                HttpClientHandler handler = new HttpClientHandler();
                handler.CookieContainer = cc;
                handler.UseCookies = true;
                Cookie cookie = new Cookie(sessionCookieName, sessionId);
                cookie.Domain = (new Uri(loginUrl)).Host;
                cc.Add(cookie);

                HttpClient httpClient = new HttpClient(handler);
                LoginResultModel result = null;
                sp.Start();

                var response = await httpClient.PostAsync(loginUrl, new FormUrlEncodedContent(parameters));
                if (response.StatusCode != HttpStatusCode.OK)
                {
                    result = new LoginResultModel();
                    result.UserName = userName;
                    try
                    {
                        result.ErrorMessage = response.Content.ReadAsAsync<HttpError>().Result.ExceptionMessage;
                    }
                    catch 
                    {
                        result.ErrorMessage = "登入錯誤(錯誤資訊無法解析),伺服器狀態碼:"+response.StatusCode;
                    }
                }
                else
                {
                    result = await response.Content.ReadAsAsync<LoginResultModel>();
                }

                sp.Stop();
                if (!string.IsNullOrEmpty(result.ErrorMessage) || sp.ElapsedMilliseconds > 100)
                    WriteLog(result, sp.ElapsedMilliseconds);

                return result;
            }
        }

        public static void WriteLog(LoginResultModel result,long
            
           

相關推薦

no