在上篇文章中我研究了OpenId及DotNetOpenAuth的相關應用,這一篇繼續研究OAuth2.
一.什么是OAuth2
OAuth是一種開放認證協議,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯系人列表),而無需將用戶名和密碼提供給第三方應用.數字2表示現在使用第2代協議.
二.OAuth2中的角色
OAuth2有四種角色
resource owner資源所有者:比如twitter用戶,他在twitter的數據就是資源,他自己就是這些資源的所有者。
resource server資源服務器:保存資源的服務器,別人要訪問受限制的資源就要出示 Access Token(訪問令牌)。
client客戶端:一個經過授權后,可以代表資源所有者訪問資源服務器上受限制資源的一方。比如 開發者開發的應用。
authorization server授權服務器:對 資源所有者進行認證,認證通過后,向 客戶端發放 Access Token(訪問令牌)。
三.認證過程
用戶訪問客戶端的網站,想操作自己存放在資源服務提供方的資源。
客戶端將用戶引導至授權服務提供方的授權頁面請求用戶授權,在這個過程中將客戶端的回調連接發送給授權服務提供方。
用戶在授權服務提供方的網頁上輸入用戶名和密碼,然后授權該客戶端訪問所請求的資源。
授權成功后,授權服務提供方對客戶端授予一個授權碼,網站跳回至客戶端。
客戶端獲得授權碼后,再次從授權服務提供方請求獲取訪問令牌 。
授權服務提供方根據授權碼授予客戶端訪問令牌。
客戶端使用獲取的訪問令牌訪問存放在資源服務提供方上的受保護的資源。
四.獲取訪問令牌方式
從上面可以看到,令牌是串起整個認證流程的核心.OAuth2有四種獲取令牌的方式
Authorization Code授權碼方式:這種是推薦使用的,也是最安全的.
Implicit Grant隱式授權:相比授權碼授權,隱式授權少了第一步的取Authorization Code的過程,而且不會返回 refresh_token。主要用於無服務器端的應用,比如 瀏覽器插件。
Resource Owner Password Credentials資源所有者密碼證書授權:這種驗證主要用於資源所有者對Client有極高的信任度的情況,比如操作系統或高權限程序。只有在不能使用其它授權方式的情況下才使用這種方式。
Client Credentials客戶端證書授權:這種情況下 Client使用自己的 client證書(如 client_id及client_secret組成的 http basic驗證碼)來獲取 access token,只能用於信任的client。
本文主要講解第一種獲取方式.
有能有些人有這樣的疑問,為什么授權成功后不直接返回訪問令牌,則是獲取授權碼,然后使用授權碼去換訪問令牌.這個問題的答案在官方的文檔里,原因主要是保障數據安全性.當用戶授權成功,瀏覽器從授權服務器返回客戶端時,數據是通過QueryString傳遞的.如果直接返回訪問令牌,則直接在地址欄可見,相關的日志系統也會記錄,這會提高令牌被破解的風險.返回授權碼,然后客戶端通過直接通信使用授權碼換取訪問令牌,整個過程對用戶是不可見的,這樣大大提高了安全性.
五.DotNetOpenAuth在OAuth2中的應用
官方Sample內包含有OAuth的完整示例,其授權服務器使用Mvc編寫,客戶端與資源服務器使用WebForm編寫,數據層使用了EF.為了更加貼進實際使用,減少無關雜音,本人模仿其重寫了一個Sample,本文的講解將圍繞自行編寫的Sample展開.Sample示例可於文后下載.
1.客戶端
客戶端編程主要圍繞三個類展開
AuthorizationServerDescription,顧名思義,用於對服務端的描述.如下所示
private static AuthorizationServerDescription AuthServerDescription; private static readonly WebServerClient Client; static OAuth2Client() { AuthServerDescription = new AuthorizationServerDescription(); AuthServerDescription.TokenEndpoint = new Uri("http://localhost:8301/OAuth/Token"); AuthServerDescription.AuthorizationEndpoint = new Uri("http://localhost:8301/OAuth/Authorize"); Client = new WebServerClient(AuthServerDescription, "sampleconsumer", "samplesecret"); }
可以看到,主要設置其兩個地址:令牌獲取地址與授權地址.然后將其作為參數來構建WebServerClient類.
WebServerClient類,是OAuth2的客戶端代理類,與授權服務器和資源服務器交互的方法都定義在上面.在實例化時需要傳入AuthServerDescription對象,客戶端名與客戶端密碼.這對名稱與密碼應該是事先向授權服務器申請的,用於標識每一個使用數據的客戶端.各個客戶端擁有各自的名稱與密碼.
生成客戶端代理后,第一件事就是應該訪問授權服務器獲取授權碼.這主要由WebServerClient類的RequestUserAuthorization方法完成.
public void RequestUserAuthorization(IEnumerable<string> scope = null, Uri returnTo = null);
在申請授權碼時,還會向授權服務器發送申請權限的范圍,參數名叫scope.一般都是一個Url地址.
申請成功,授權服務器返回后,客戶端需再次訪問授權服務器申請訪問令牌.這主要由WebServerClient類的ProcessUserAuthorization方法完成
public IAuthorizationState ProcessUserAuthorization(HttpRequestBase request = null);
成功申請后,會返回一個IAuthorizationState接口對象,其定義如下
string AccessToken { get; set; } DateTime? AccessTokenExpirationUtc { get; set; } DateTime? AccessTokenIssueDateUtc { get; set; } Uri Callback { get; set; } string RefreshToken { get; set; } HashSet<string> Scope { get; }
很好理解,AccessToken為訪問令牌,RefreshToken為刷新令牌,AccessTokenIssueDateUtc為訪問令牌生成時間,AccessTokenExpirationUtc為訪問令牌過期時間,Callback為回調的Url,Scope為權限的范圍,或者叫被授權可以訪問的地址范圍.
在Sample中為了簡化編程對框架作了二次封裝,如下
1 private static AuthorizationServerDescription AuthServerDescription; 2 3 private static readonly WebServerClient Client; 4 5 static OAuth2Client() 6 { 7 AuthServerDescription = new AuthorizationServerDescription(); 8 AuthServerDescription.TokenEndpoint = new Uri("http://localhost:8301/OAuth/Token"); 9 AuthServerDescription.AuthorizationEndpoint = new Uri("http://localhost:8301/OAuth/Authorize"); 10 11 Client = new WebServerClient(AuthServerDescription, "sampleconsumer", "samplesecret"); 12 } 13 14 private static IAuthorizationState Authorization 15 { 16 get { return (AuthorizationState)HttpContext.Current.Session["Authorization"]; } 17 set { HttpContext.Current.Session["Authorization"] = value; } 18 } 19 20 public static void GetUserAuthorization(string scope) 21 { 22 GetUserAuthorization(new string[] { scope }); 23 } 24 25 public static void GetUserAuthorization(IEnumerable<string> scopes) 26 { 27 if (Authorization != null) 28 { 29 return; 30 } 31 32 IAuthorizationState authorization = Client.ProcessUserAuthorization(); 33 if (authorization == null) 34 { 35 Client.RequestUserAuthorization(scopes); 36 37 return; 38 } 39 40 Authorization = authorization; 41 HttpContext.Current.Response.Redirect(HttpContext.Current.Request.Path); 42 }
前12行為對象初始化,14到18行將獲取的權限對象保存在Session中,屬性名為Authorization.客戶端使用GetUserAuthorization方法來獲取對某地址訪問授權.
在頁面中調用代碼如下
if (!IsPostBack) { OAuth2Client.GetUserAuthorization("http://tempuri.org/IGetData/NameLength"); }
打開頁面,首次調用GetUserAuthorization方法后,首先判斷權限對象Authorization是否為空.不為空說明已獲取到權限.為空則執行ProcessUserAuthorization方法獲取訪問令牌,由於此時沒有授權碼,則返回的權限對象為空.最后通過RequestUserAuthorization方法向授權服務器申請授權碼.
獲取成功后,瀏覽器頁面會刷新,在頁面地址后追加了授權碼.此時第二次執行GetUserAuthorization方法.權限對象Authorization仍然為空,但由於已有授權碼,則ProcessUserAuthorization方法將向授權服務器申請訪問令牌.獲取成功后將返回的權限對象賦給Authorization屬性,然后再次刷新本頁面.注意,刷新地址使用的是HttpContext.Current.Request.Path,而此屬性是不包括QueryString的.作用是將授權碼從地址欄中去除.
第三次執行GetUserAuthorization方法,由於權限對象Authorization已不為空,則直接返回.
訪問令牌默認是有時效的.當過期后,要么走上面三步重新申請一個令牌,不過更好的方法是使用刷新令牌刷新訪問令牌.這主要由WebServerClient類的RefreshAuthorization方法完成
public bool RefreshAuthorization(IAuthorizationState authorization, TimeSpan? skipIfUsefulLifeExceeds = null);
使用訪問令牌的方式,是將令牌添加到訪問資源服務器Http請求的頭上,這主要由WebServerClient類的AuthorizeRequest方法完成
public void AuthorizeRequest(HttpWebRequest request, IAuthorizationState authorization); public void AuthorizeRequest(WebHeaderCollection requestHeaders, IAuthorizationState authorization);
在Sample中針對Wcf請求作了二次封裝,如下
1 public static TReturn UseService<TService, TReturn>(Expression<Func<TService, TReturn>> operation) 2 { 3 if (Authorization.AccessTokenExpirationUtc.HasValue) 4 { 5 Client.RefreshAuthorization(Authorization, TimeSpan.FromMinutes(2)); 6 } 7 8 TService channel = new ChannelFactory<TService>("*").CreateChannel(); 9 IClientChannel client = (IClientChannel)channel; 10 11 HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(client.RemoteAddress.Uri); 12 ClientBase.AuthorizeRequest(httpRequest, Authorization.AccessToken); 13 HttpRequestMessageProperty httpDetails = new HttpRequestMessageProperty(); 14 httpDetails.Headers[HttpRequestHeader.Authorization] = httpRequest.Headers[HttpRequestHeader.Authorization]; 15 16 using (OperationContextScope scope = new OperationContextScope(client)) 17 { 18 OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpDetails; 19 20 client.Open(); 21 TReturn result = operation.Compile().Invoke(channel); 22 try 23 { 24 client.Close(); 25 } 26 catch 27 { 28 client.Abort(); 29 throw; 30 } 31 32 return result; 33 } 34 }
在請求一個Wcf前,首先判斷有效期.如果少於2分鍾則首先刷新訪問令牌.之后構建一個HttpWebRequest對象,並使用AuthorizeRequest方法將訪問令牌添加在請求頭上.從第13行之后是Wcf的特定寫法,其中13到18行表示將Http授權頭賦給Wcf授權頭.
2.授權服務端
服務端要做的事其實很好理解,就是記錄某用戶在某客戶端的授權情況.其使用數據庫來保存相關信息.Client表存儲客戶端,User表存儲用戶,ClientAuthorization表是張關系表,存儲某用戶在某客戶端授予的權限.Nonce存儲訪問隨機數,SymmertricCryptoKey表存儲對稱加密的密碼.
服務端主要圍繞以下對象編程
AuthorizationServer類,代表授權服務類.主要的功能都由它提供.IAuthorizationServerHost接口是編寫驗證邏輯的地方,由OAuth2AuthorizationServer類實現,ICryptoKeyStore是訪問密碼的接口,INonceStore是訪問隨機數的地方,這兩個接口由DatabaseKeyNonceStore類實現,IClientDescription是描述客戶端的接口,由Client實現.
在本Sample中,OpenId與OAuth2是配合使用的.用戶需要先去OpenId進行登錄,然后去OAuth2進行授權.從這個意義上講,OAuth2受OpenId的統一管理,是其一個客戶端.
AccountController是一個典型的OpenId客戶端編程.上篇文章已有講解,故不贅述.
當客戶端申請授權碼時,首先執行OAuthController類的Authorize方法,如下,有刪節
public ActionResult Authorize() { var pendingRequest = this.authorizationServer.ReadAuthorizationRequest(); if ((this.authorizationServer.AuthorizationServerServices as OAuth2AuthorizationServer).CanBeAutoApproved(pendingRequest)) { var approval = this.authorizationServer.PrepareApproveAuthorizationRequest(pendingRequest, HttpContext.User.Identity.Name); return this.authorizationServer.Channel.PrepareResponse(approval).AsActionResult(); } database.AddParameter("@ClientIdentifier", pendingRequest.ClientIdentifier); ViewBag.Name = database.ExecuteScalar("select name from Client where ClientIdentifier = @ClientIdentifier").ToString(); ViewBag.AuthorizationRequest = pendingRequest; return View(); }
AuthorizationServer類的ReadAuthorizationRequest方法會獲取用戶請求並返回一個EndUserAuthorizationRequest對象,此對象定義如下
public Uri Callback { get; set; } public string ClientIdentifier { get; set; } public string ClientState { get; set; } public virtual EndUserAuthorizationResponseType ResponseType { get; } public HashSet<string> Scope { get; }
可以看到包括了客戶端的相關信息.然后將此對象傳入OAuth2AuthorizationServer對像的CanBeAutoApproved方法,查看能否自動發放授權碼.
public bool CanBeAutoApproved(EndUserAuthorizationRequest authorizationRequest) { if (authorizationRequest.ResponseType == EndUserAuthorizationResponseType.AuthorizationCode) { database.AddParameter("@ClientIdentifier", authorizationRequest.ClientIdentifier); object result = database.ExecuteScalar("select ClientSecret from client where ClientIdentifier = @ClientIdentifier"); if (result != null && !string.IsNullOrEmpty(result.ToString())) { return this.IsAuthorizationValid(authorizationRequest.Scope, authorizationRequest.ClientIdentifier, DateTime.UtcNow, HttpContext.Current.User.Identity.Name); } } return false; }
此方法是查找數據庫中有無此客戶端記錄且密碼不為空,如果不為空且處於獲取授權碼階段,則會調用了IsAuthorizationValid方法
private bool IsAuthorizationValid(HashSet<string> requestedScopes, string clientIdentifier, DateTime issuedUtc, string username) { issuedUtc += TimeSpan.FromSeconds(1); database.AddParameter("@ClientIdentifier", clientIdentifier); database.AddParameter("@CreatedOnUtc", issuedUtc); database.AddParameter("@ExpirationDateUtc", DateTime.UtcNow); database.AddParameter("@OpenIDClaimedIdentifier", username); StringBuilder sb = new StringBuilder(); sb.Append("select scope from [user] u "); sb.Append(" join ClientAuthorization ca on u.userid = ca.userid "); sb.Append(" join Client c on c.clientid = ca.clientid "); sb.Append(" where c.ClientIdentifier = @ClientIdentifier "); sb.Append(" and CreatedOnUtc <= @CreatedOnUtc"); sb.Append(" and ( ExpirationDateUtc is null or ExpirationDateUtc >= @ExpirationDateUtc ) "); sb.Append(" and u.OpenIDClaimedIdentifier = @OpenIDClaimedIdentifier "); DataTable dt = database.ExecuteDataSet(sb.ToString()).Tables[0]; if (dt.Rows.Count == 0) { return false; } var grantedScopes = new HashSet<string>(OAuthUtilities.ScopeStringComparer); foreach (DataRow dr in dt.Rows) { grantedScopes.UnionWith(OAuthUtilities.SplitScopes(dr["scope"].ToString())); } return requestedScopes.IsSubsetOf(grantedScopes); }
可以看到,此方法查找指定用戶在指定客戶端上是否有對目標范圍的授權,且沒有過期.也就是說,如果客服端的密碼不能為空,且當前用戶在此客戶端上對目標范圍還有未過期的授權,則自動發放授權碼.
回到最初的Authorize方法.如果可以自動發放授權碼,則調用AuthorizationServer類的PrepareApproveAuthorizationRequest方法生成一個授權碼,並通過AuthorizationServer類Channel屬性的PrepareResponse方法最終返回給客戶端.
如果不能自動發放,則瀏覽器會跳轉到一個確認頁面,如下圖所示
點擊后執行OAuthController類的AuthorizeResponse方法,有刪節.
public ActionResult AuthorizeResponse(bool isApproved) { var pendingRequest = this.authorizationServer.ReadAuthorizationRequest(); IDirectedProtocolMessage response; if (isApproved) { database.AddParameter("@ClientIdentifier", pendingRequest.ClientIdentifier); int clientId = Convert.ToInt32(database.ExecuteScalar("select clientId from client where ClientIdentifier = @ClientIdentifier")); database.AddParameter("@OpenIDClaimedIdentifier", User.Identity.Name); int userId = Convert.ToInt32(database.ExecuteScalar("select userId from [user] where OpenIDClaimedIdentifier = @OpenIDClaimedIdentifier")); database.AddParameter("@CreatedOnUtc", DateTime.UtcNow); database.AddParameter("@clientId", clientId); database.AddParameter("@userId", userId); database.AddParameter("@Scope", OAuthUtilities.JoinScopes(pendingRequest.Scope)); database.ExecuteNonQuery("insert into ClientAuthorization values(null, @CreatedOnUtc, @clientId, @userId, @Scope, null)"); response = this.authorizationServer.PrepareApproveAuthorizationRequest(pendingRequest, User.Identity.Name); } else { response = this.authorizationServer.PrepareRejectAuthorizationRequest(pendingRequest); } return this.authorizationServer.Channel.PrepareResponse(response).AsActionResult(); }
邏輯比較簡單,如果同意,則獲取客戶端信息后,在數據庫的ClientAuthorization表中插入某時某用戶在某客戶端對於某訪問范圍的權限信息,然后如同上面一樣,調用AuthorizationServer類的PrepareApproveAuthorizationRequest方法生成一個授權碼,並通過AuthorizationServer類Channel屬性的PrepareResponse方法最終返回給客戶端.
有一點需要注意.Authorize方法是從請求中獲取客戶端信息,而AuthorizeResponse方法則是從Authorize方法所對應的View中獲取客戶端信息.所以此View必需包含相關系統.在Sample中我首先將獲取出來的pendingRequest對象賦於ViewBag.AuthorizationRequest,然后在View中將其放入隱藏域.注意,其名字是固定的.
@{ ViewBag.Title = "Authorize"; Layout = "~/Views/Shared/_Layout.cshtml"; DotNetOpenAuth.OAuth2.Messages.EndUserAuthorizationRequest AuthorizationRequest = ViewBag.AuthorizationRequest; } <h2> Authorize</h2> 是否授權 @ViewBag.Name 訪問以下地址 <hr /> @foreach (string scope in AuthorizationRequest.Scope) { @scope <br /> } @using (Html.BeginForm("AuthorizeResponse", "OAuth")) { @Html.AntiForgeryToken() @Html.Hidden("isApproved") @Html.Hidden("client_id", AuthorizationRequest.ClientIdentifier) @Html.Hidden("redirect_uri", AuthorizationRequest.Callback) @Html.Hidden("state", AuthorizationRequest.ClientState) @Html.Hidden("scope", DotNetOpenAuth.OAuth2.OAuthUtilities.JoinScopes(AuthorizationRequest.Scope)) @Html.Hidden("response_type", AuthorizationRequest.ResponseType == DotNetOpenAuth.OAuth2.Messages.EndUserAuthorizationResponseType.AccessToken ? "token" : "code") <div> <input type="submit" value="Yes" onclick="document.getElementById('isApproved').value = true; return true;" /> <input type="submit" value="No" onclick="document.getElementById('isApproved').value = false; return true;" /> </div> }
此時客戶端已獲取到授權碼.然后會發出第二次請求申請訪問令牌.這個請求由OAuthController類的Token方法處理
public ActionResult Token() { return this.authorizationServer.HandleTokenRequest(this.Request).AsActionResult(); }
實際上由AuthorizationServer類的HandleTokenRequest方法處理,最終調用OAuth2AuthorizationServer類的CreateAccessToken方法創建訪問令牌並返回客戶端.
大體的服務端編程接口分析到此結束,下面我們深入源碼來理解這些關鍵類的架構模式.
AuthorizationServer類主要提供編程接口,而自行實現的OAuth2AuthorizationServer類,DatabaseKeyNonceStore類和Client類則主要負責與數據庫的交互.真正負責通信的是Channel抽象類,其作為AuthorizationServer類的Channel屬性對外公布,具體實現類為OAuth2AuthorizationServerChannel類.
在Channel類上作者使用了一種類似於Asp.Net的管道模型的方式來架構此類.相對於IHttpModule接口,這里的接口名叫IChannelBindingElement.其定義如下
public interface IChannelBindingElement { Channel Channel { get; set; } MessageProtections Protection { get; } MessageProtections? ProcessOutgoingMessage(IProtocolMessage message); MessageProtections? ProcessIncomingMessage(IProtocolMessage message); }
而在Channel類中的關鍵部分如下
private readonly List<IChannelBindingElement> outgoingBindingElements = new List<IChannelBindingElement>(); private readonly List<IChannelBindingElement> incomingBindingElements = new List<IChannelBindingElement>(); protected Channel(IMessageFactory messageTypeProvider, params IChannelBindingElement[] bindingElements) { ... this.outgoingBindingElements = new List<IChannelBindingElement>(ValidateAndPrepareBindingElements(bindingElements)); this.incomingBindingElements = new List<IChannelBindingElement>(this.outgoingBindingElements); this.incomingBindingElements.Reverse(); ... } protected virtual void ProcessIncomingMessage(IProtocolMessage message) { foreach (IChannelBindingElement bindingElement in this.IncomingBindingElements) { ...
MessageProtections? elementProtection = bindingElement.ProcessIncomingMessage(message);
...
} ... } protected void ProcessOutgoingMessage(IProtocolMessage message) { foreach (IChannelBindingElement bindingElement in this.outgoingBindingElements) { ...
MessageProtections? elementProtection = bindingElement.ProcessOutgoingMessage(message);
... } ... }
可以看到定義了兩個集合分別存儲請求過濾器與響應過濾器.兩者都由構造函數初始化,內容一樣,順序相反.在讀取請求時會遍歷IncomingBindingElements集合並逐個調用ProcessIncomingMessage方法對傳入的message進行處理,在發出響應時會遍歷outgoingBindingElements集合並逐個調用ProcessOutgoingMessage方法對message進行處理.
下面就以授權服務器接收授權碼並發送訪問令牌為例來分析此架構在實例中的應用.
上面講過,客戶端發送請求后,由OAuthController類的Token方法響應
public ActionResult Token() { return this.authorizationServer.HandleTokenRequest(this.Request).AsActionResult(); }
調用了AuthorizationServer類的HandleTokenRequest方法,有刪節
public OutgoingWebResponse HandleTokenRequest(HttpRequestBase request = null) { try { if (this.Channel.TryReadFromRequest(request, out requestMessage)) { var accessTokenResult = this.AuthorizationServerServices.CreateAccessToken(requestMessage); ... } ... } ... return this.Channel.PrepareResponse(responseMessage); }
可以看到,實際都用調用Channel中的方法,讀取請求調用的TryReadFromRequest方法
public bool TryReadFromRequest<TRequest>(HttpRequestBase httpRequest, out TRequest request) where TRequest : class, IProtocolMessage { ... IProtocolMessage untypedRequest = this.ReadFromRequest(httpRequest); ... }
之后調用了自身的ReadFromRequest方法
public IDirectedProtocolMessage ReadFromRequest(HttpRequestBase httpRequest) { IDirectedProtocolMessage requestMessage = this.ReadFromRequestCore(httpRequest); if (requestMessage != null) { var directRequest = requestMessage as IHttpDirectRequest; if (directRequest != null) { foreach (string header in httpRequest.Headers) { directRequest.Headers[header] = httpRequest.Headers[header]; } } this.ProcessIncomingMessage(requestMessage); } return requestMessage; }
可以看到,這里就會調用ProcessIncomingMessage方法對通過ReadFromRequestCore方法讀取到請求作過濾
回到HandleTokenRequest方法,當其調用AuthorizationServerServices屬性的CreateAccessToken方法生成訪問令牌后,會調用Channel屬性的PrepareResponse方法生成響應
public OutgoingWebResponse PrepareResponse(IProtocolMessage message) { ... this.ProcessOutgoingMessage(message); ... OutgoingWebResponse result; switch (message.Transport) { case MessageTransport.Direct: result = this.PrepareDirectResponse(message); break; ... } result.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, max-age=0, must-revalidate"; result.Headers[HttpResponseHeader.Pragma] = "no-cache"; return result; }
可以看到,首先就調用了ProcessOutgoingMessage方法過濾響應,然后調用PrepareDirectResponse方法最終生成響應
下面繼續分析其過濾器組件的實現.
我們在使用AuthorizationServer類時,其Channel屬性的實際類型是OAuth2AuthorizationServerChannel類.此類的構造函數會調用本類InitializeBindingElements靜態方法加載兩個IChannelBindingElement類型的過濾器,然后傳入父類構造函數,最終會被添加到上文所說的Channel類的outgoingBindingElements集合與incomingBindingElements集合中.
protected internal OAuth2AuthorizationServerChannel(IAuthorizationServerHost authorizationServer, ClientAuthenticationModule clientAuthenticationModule) : base(MessageTypes, InitializeBindingElements(authorizationServer, clientAuthenticationModule)) { Requires.NotNull(authorizationServer, "authorizationServer"); this.AuthorizationServer = authorizationServer; } private static IChannelBindingElement[] InitializeBindingElements(IAuthorizationServerHost authorizationServer, ClientAuthenticationModule clientAuthenticationModule) { ... var bindingElements = new List<IChannelBindingElement>(); bindingElements.Add(new MessageValidationBindingElement(clientAuthenticationModule)); bindingElements.Add(new TokenCodeSerializationBindingElement()); return bindingElements.ToArray(); }
從功能上講,MessageValidationBindingElement負責驗證,TokenCodeSerializationBindingElement負責加解密,數字簽名,請求保護等,從順序上講,讀取請求時先執行后者再執行前者,發送響應時反之.
首先查看MessageValidationBindingElement類
private readonly ClientAuthenticationModule clientAuthenticationModule; internal MessageValidationBindingElement(ClientAuthenticationModule clientAuthenticationModule) { this.clientAuthenticationModule = clientAuthenticationModule; } public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { ... if (authenticatedClientRequest != null) { ... var result = this.clientAuthenticationModule.TryAuthenticateClient(this.AuthServerChannel.AuthorizationServer, authenticatedClientRequest, out clientIdentifier); ... } ... }
即然是驗證客戶端,那么只需要在讀取請求時執行即可,可以看到此類將實際驗證又委托給了ClientAuthenticationModule類的TryAuthenticateClient方法.
public abstract class ClientAuthenticationModule { public abstract ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier); protected static ClientAuthenticationResult TryAuthenticateClientBySecret(IAuthorizationServerHost authorizationServerHost, string clientIdentifier, string clientSecret) {...
} }
可以看到此類是個抽象類.在實際中真正執行的是ClientCredentialHttpBasicReader類與ClientCredentialMessagePartReader類,各自重寫的TryAuthenticateClient方法其際調用的都是基類的TryAuthenticateClientBySecret靜態方法.
public class ClientCredentialHttpBasicReader : ClientAuthenticationModule { public override ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier) { ... var credential = OAuthUtilities.ParseHttpBasicAuth(requestMessage.Headers); if (credential != null) { clientIdentifier = credential.UserName; return TryAuthenticateClientBySecret(authorizationServerHost, credential.UserName, credential.Password); } clientIdentifier = null; return ClientAuthenticationResult.NoAuthenticationRecognized; } } public class ClientCredentialMessagePartReader : ClientAuthenticationModule { public override ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier) { ... clientIdentifier = requestMessage.ClientIdentifier; return TryAuthenticateClientBySecret(authorizationServerHost, requestMessage.ClientIdentifier, requestMessage.ClientSecret); } }
有意思的是,在實際使用中實現了InitializeBindingElements接口的MessageValidationBindingElement類並不直接調用實現了ClientAuthenticationModule抽象類的上面的兩者,而是在中間又加入了一個AggregatingClientCredentialReader類,有點像代理模式,整個邏輯的關鍵代碼如下,有刪節
public class AuthorizationServer { private readonly List<ClientAuthenticationModule> clientAuthenticationModules = new List<ClientAuthenticationModule>(); private readonly ClientAuthenticationModule aggregatingClientAuthenticationModule; public AuthorizationServer(IAuthorizationServerHost authorizationServer) { this.clientAuthenticationModules.AddRange(OAuth2AuthorizationServerSection.Configuration.ClientAuthenticationModules.CreateInstances(true)); this.aggregatingClientAuthenticationModule = new AggregatingClientCredentialReader(this.clientAuthenticationModules); this.Channel = new OAuth2AuthorizationServerChannel(authorizationServer, this.aggregatingClientAuthenticationModule); ... } } internal class OAuth2AuthorizationServerSection : ConfigurationSection { private static readonly TypeConfigurationCollection<ClientAuthenticationModule> defaultClientAuthenticationModules = new TypeConfigurationCollection<ClientAuthenticationModule>(new Type[] { typeof(ClientCredentialHttpBasicReader), typeof(ClientCredentialMessagePartReader) }); internal static OAuth2AuthorizationServerSection Configuration { get { return (OAuth2AuthorizationServerSection)ConfigurationManager.GetSection(SectionName) ?? new OAuth2AuthorizationServerSection(); } } internal TypeConfigurationCollection<ClientAuthenticationModule> ClientAuthenticationModules { get { var configResult = (TypeConfigurationCollection<ClientAuthenticationModule>)this[ClientAuthenticationModulesElementName]; return configResult != null && configResult.Count > 0 ? configResult : defaultClientAuthenticationModules; } ... } }
可以看到,在創建AuthorizationServer類時,就會從OAuth2AuthorizationServerSection類,也就是配置文件中獲取ClientAuthenticationModule類名.如果沒有任何配置,則使用默認的ClientCredentialHttpBasicReader類與ClientCredentialMessagePartReader類.然后將獲取的ClientAuthenticationModule類集合作為參數創建AggregatingClientCredentialReader類,最后將AggregatingClientCredentialReader類實例作為參數傳入Channel中,就如上文所說,包裝為實現了InitializeBindingElements接口的MessageValidationBindingElement類.
上文說過了,MessageValidationBindingElement類只與ClientAuthenticationModule抽象類交互,所以AggregatingClientCredentialReader類也實現了ClientAuthenticationModule抽象類
internal class AggregatingClientCredentialReader : ClientAuthenticationModule { private readonly IEnumerable<ClientAuthenticationModule> authenticators; internal AggregatingClientCredentialReader(IEnumerable<ClientAuthenticationModule> authenticators) { this.authenticators = authenticators; } public override ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier) { ... foreach (var candidateAuthenticator in this.authenticators) { string candidateClientIdentifier; var resultCandidate = candidateAuthenticator.TryAuthenticateClient(authorizationServerHost, requestMessage, out candidateClientIdentifier); ... } ... } }
如上文所說,這很像一個代理代,其內部保存了傳入的ClientAuthenticationModule類集合,並實現了ClientAuthenticationModule抽象類.調用抽象方法TryAuthenticateClient最終會轉變為遍歷ClientAuthenticationModule集合並逐個調用.
回到ClientAuthenticationModule類的靜態方法TryAuthenticateClientBySecret,這也是MessageValidationBindingElement類實現客戶端研究的核心方法
protected static ClientAuthenticationResult TryAuthenticateClientBySecret(IAuthorizationServerHost authorizationServerHost, string clientIdentifier, string clientSecret) { if (!string.IsNullOrEmpty(clientIdentifier)) { var client = authorizationServerHost.GetClient(clientIdentifier); if (client != null) { if (!string.IsNullOrEmpty(clientSecret)) { if (client.IsValidClientSecret(clientSecret)) { ... } } } } ... }
可以看到,它實際上使用了我們自己寫的IAuthorizationServerHost接口實現類OAuth2AuthorizationServer,從數據庫中獲取相關信息驗證客戶端.首先調用GetClient方法查找客戶端,如果存在,則調用Client對象的IsValidClientSecret方法驗證密碼是否正確.
上文說過MessageValidationBindingElement類主要用來作驗證.除了調用ClientAuthenticationModule類驗證客戶名密碼外,還做了很多其它方面的驗證,比如客戶端的CallbackUrl是否合法與一致,這通過調用Client類的IsCallbackAllowed方法與DefaultCallback屬性完成.請求令牌的客戶端是否就是我們即將發送令牌的客戶端,客戶端請求的權限范圍沒有超出在授權服務器申請的權限范圍,令牌還未被注銷或過期之類的.這實際上調用了OAuth2AuthorizationServer類的IsAuthorizationValid方法.
下面來看一下TokenCodeSerializationBindingElement類
首先再回顧一下授權過程,客戶端第一次向授權服務器發出請求,返回授權碼,然后客戶端第二次使用授權碼向授權服務端發出請求,返回訪問令牌,如果客戶端需要刷新訪問令牌,則向授權服務器發送刷新令牌,返回訪問令牌.這里有三個重要對象:授權碼,刷新令牌,訪問令牌.對於前兩者,授權服務器是既可能接收也可能發送,對於最后者,只會發送不會接收.TokenCodeSerializationBindingElement類就是按這么來設計的.
public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { // Serialize the authorization code, if there is one. var authCodeCarrier = message as IAuthorizationCodeCarryingRequest; if (authCodeCarrier != null) { var codeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); var code = authCodeCarrier.AuthorizationDescription; authCodeCarrier.Code = codeFormatter.Serialize(code); return MessageProtections.None; } // Serialize the refresh token, if applicable. var refreshTokenResponse = message as AccessTokenSuccessResponse; if (refreshTokenResponse != null && refreshTokenResponse.HasRefreshToken) { var refreshTokenCarrier = (IAuthorizationCarryingRequest)message; var refreshToken = new RefreshToken(refreshTokenCarrier.AuthorizationDescription); var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); refreshTokenResponse.RefreshToken = refreshTokenFormatter.Serialize(refreshToken); } // Serialize the access token, if applicable. var accessTokenResponse = message as IAccessTokenIssuingResponse; if (accessTokenResponse != null && accessTokenResponse.AuthorizationDescription != null) { ErrorUtilities.VerifyInternal(request != null, "We should always have a direct request message for this case."); accessTokenResponse.AccessToken = accessTokenResponse.AuthorizationDescription.Serialize(); } return null; } public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { var authCodeCarrier = message as IAuthorizationCodeCarryingRequest; if (authCodeCarrier != null) { var authorizationCodeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); var authorizationCode = new AuthorizationCode(); authorizationCodeFormatter.Deserialize(authorizationCode, authCodeCarrier.Code, message, Protocol.code); authCodeCarrier.AuthorizationDescription = authorizationCode; } var refreshTokenCarrier = message as IRefreshTokenCarryingRequest; if (refreshTokenCarrier != null) { var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); var refreshToken = new RefreshToken(); refreshTokenFormatter.Deserialize(refreshToken, refreshTokenCarrier.RefreshToken, message, Protocol.refresh_token); refreshTokenCarrier.AuthorizationDescription = refreshToken; } return null; }
AuthorizationCode對應授權碼,RefreshToken對應刷新令牌,AccessToken類與AuthorizationServerAccessToken對應訪問令牌.在發送響應前,三者都可能被序列化,在接收請求后,只會對前兩者進行可能的反序列化.
對於前兩者,序列化與反序列化都是直接調用類的靜態方法CreateFormatter創建序列化器,然后再進行操作
internal class RefreshToken : AuthorizationDataBag { internal static IDataBagFormatter<RefreshToken> CreateFormatter(ICryptoKeyStore cryptoKeyStore) { return new UriStyleMessageFormatter<RefreshToken>(cryptoKeyStore, RefreshTokenKeyBucket, signed: true, encrypted: true); } } internal class AuthorizationCode : AuthorizationDataBag { internal static IDataBagFormatter<AuthorizationCode> CreateFormatter(IAuthorizationServerHost authorizationServer) { return new UriStyleMessageFormatter<AuthorizationCode>( cryptoStore, AuthorizationCodeKeyBucket, signed: true, encrypted: true, compressed: false, maximumAge: MaximumMessageAge, decodeOnceOnly: authorizationServer.NonceStore); } }
訪問令牌則是通過AuthorizationServerAccessToken類的實例方法Serialize調用AccessToken類的靜態方法CreateFormatter來創建序列化器
public class AuthorizationServerAccessToken : AccessToken { protected internal override string Serialize() { var formatter = CreateFormatter(this.AccessTokenSigningKey, this.ResourceServerEncryptionKey); return formatter.Serialize(this); } } public class AccessToken : AuthorizationDataBag { internal static IDataBagFormatter<AccessToken> CreateFormatter(RSACryptoServiceProvider signingKey, RSACryptoServiceProvider encryptingKey) { return new UriStyleMessageFormatter<AccessToken>(signingKey, encryptingKey); } }
這里統一使用了UriStyleMessageFormatter<T>類作為序列化器,而Serialize與Deserialize方法實際上是從其基類DataBagFormatterBase<T>繼承過來的.
private const int NonceLength = 6; private readonly TimeSpan minimumAge = TimeSpan.FromDays(1); private readonly ICryptoKeyStore cryptoKeyStore; private readonly string cryptoKeyBucket; private readonly RSACryptoServiceProvider asymmetricSigning; private readonly RSACryptoServiceProvider asymmetricEncrypting; private readonly bool signed; private readonly INonceStore decodeOnceOnly; private readonly TimeSpan? maximumAge; private readonly bool encrypted; private readonly bool compressed; protected DataBagFormatterBase(RSACryptoServiceProvider signingKey = null, RSACryptoServiceProvider encryptingKey = null, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null); protected DataBagFormatterBase(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null); private DataBagFormatterBase(bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null);
可以看到,我們實現的關於密碼存儲的兩個接口在這里出現了.它們和對稱加密器RSACryptoServiceProvider一同通過構造函數傳入.
作者使用了一種名為Nonce的技術提高網站安全性.Nonce是由服務器生成的一個隨機數,在客戶端第一次請求頁面時將其發回客戶端;客戶端拿到這個Nonce,將其與用戶密碼串聯在一起並進行非可逆加密(MD5、SHA1等等),然后將這個加密后的字符串和用戶名、Nonce、 加密算法名稱一起發回服務器;服務器使用接收到的用戶名到數據庫搜索密碼,然后跟客戶端使用同樣的算法對其進行加密,接着將其與客戶端提交上來的加密字符 串進行比較,如果兩個字符串一致就表示用戶身份有效。這樣就解決了用戶密碼明文被竊取的問題,攻擊者就算知道了算法名和nonce也無法解密出密碼。
每個nonce只能供一個用戶使用一次,這樣就可以防止攻擊者使用重放攻擊,因為該Http報文已經無效。可選的實現方式是把每一次請求的Nonce保存到數據庫,客戶端再一次提交請求時將請求頭中得Nonce與數據庫中得數據作比較,如果已存在該Nonce,則證明該請求有可能是惡意的。然而這種解決方案也有個問題,很有可能在兩次正常的資源請求中,產生的隨機數是一樣的,這樣就造成正常的請求也被當成了攻擊,隨着數據庫中保存的隨機數不斷增多,這個問題就會變得很明顯。所以,還需要加上另外一個參數Timestamp(時間戳)。
Timestamp是根據服務器當前時間生成的一個字符串,與nonce放在一起,可以表示服務器在某個時間點生成的隨機數。這樣就算生成的隨機數相同,但因為它們生成的時間點不一樣,所以也算有效的隨機數。
對於授權碼與刷新令牌,由於僅用於客戶端與授權服務器使用,且在客戶端不需要對其進行解秘,作者使用了對稱加密技術來保障其安全.對稱加密密鑰是隨機生成的.
對於訪問令牌,當由授權服務器發送給客戶端后,客戶端需要將其發送到資源服務器進行驗證,作者使用了授權服務器公/密鑰,資源服務器公/密鑰,兩套非對稱加密技術來保障其安全.首先使用資源服務器公鑰加密,然后使用授權服務器密鑰簽名.資源服務器使用時,先通過授權服務器公鑰驗證數字簽名保證訪問令牌合法,然后使用自身的資源服務器密鑰解密獲取相關信息.
參考
下面來看一下Serialize函數
1 public string Serialize(T message) 2 { 3 if (this.decodeOnceOnly != null) 4 { 5 message.Nonce = MessagingUtilities.GetNonCryptoRandomData(NonceLength); 6 } 7 8 byte[] encoded = this.SerializeCore(message); 9 10 if (this.compressed) 11 { 12 encoded = MessagingUtilities.Compress(encoded); 13 } 14 15 string symmetricSecretHandle = null; 16 if (this.encrypted) 17 { 18 encoded = this.Encrypt(encoded, out symmetricSecretHandle); 19 } 20 21 if (this.signed) 22 { 23 message.Signature = this.CalculateSignature(encoded, symmetricSecretHandle); 24 } 25 26 int capacity = this.signed ? 4 + message.Signature.Length + 4 + encoded.Length : encoded.Length; 27 using (var finalStream = new MemoryStream(capacity)) 28 { 29 var writer = new BinaryWriter(finalStream); 30 if (this.signed) 31 { 32 writer.WriteBuffer(message.Signature); 33 } 34 35 writer.WriteBuffer(encoded); 36 writer.Flush(); 37 38 string payload = MessagingUtilities.ConvertToBase64WebSafeString(finalStream.ToArray()); 39 string result = payload; 40 if (symmetricSecretHandle != null && (this.signed || this.encrypted)) 41 { 42 result = MessagingUtilities.CombineKeyHandleAndPayload(symmetricSecretHandle, payload); 43 } 44 45 return result; 46 } 47 }
代碼第5行,生成一個Nonce隨機數並保存在消息中,其本質使用了System.Random類
public static class MessagingUtilities { internal static Random NonCryptoRandomDataGenerator { get { return ThreadSafeRandom.RandomNumberGenerator; } } internal static byte[] GetNonCryptoRandomData(int length) { byte[] buffer = new byte[length]; NonCryptoRandomDataGenerator.NextBytes(buffer); return buffer; } } private static class ThreadSafeRandom { [ThreadStatic] private static Random threadRandom; public static Random RandomNumberGenerator { get { if (threadRandom == null) { lock (threadRandomInitializer) { threadRandom = new Random(threadRandomInitializer.Next()); } } return threadRandom; } } }
第8行將消息序列化成一般二進制流.這個方法等一下再講
第12行將流壓縮,采用Deflate或Gzip壓縮
internal static byte[] Compress(byte[] buffer, CompressionMethod method = CompressionMethod.Deflate) { using (var ms = new MemoryStream()) { Stream compressingStream = null; try { switch (method) { case CompressionMethod.Deflate: compressingStream = new DeflateStream(ms, CompressionMode.Compress, true); break; case CompressionMethod.Gzip: compressingStream = new GZipStream(ms, CompressionMode.Compress, true); break; } compressingStream.Write(buffer, 0, buffer.Length); return ms.ToArray(); } finally { if (compressingStream != null) { compressingStream.Dispose(); } } } }
第18行進行加密並獲取可能的隨機生成的對稱加密密鑰
private byte[] Encrypt(byte[] value, out string symmetricSecretHandle) { if (this.asymmetricEncrypting != null) { symmetricSecretHandle = null; return this.asymmetricEncrypting.EncryptWithRandomSymmetricKey(value); } else { var cryptoKey = this.cryptoKeyStore.GetCurrentKey(this.cryptoKeyBucket, this.minimumAge); symmetricSecretHandle = cryptoKey.Key; return MessagingUtilities.Encrypt(value, cryptoKey.Value.Key); } }
如果使用非對稱加密,則使用EncryptWithRandomSymmetricKey方法.這是作者自行定義的擴展方法
internal static byte[] EncryptWithRandomSymmetricKey(this RSACryptoServiceProvider crypto, byte[] buffer);
如果使用對稱加密,則調用GetCurrentKey方法
internal static KeyValuePair<string, CryptoKey> GetCurrentKey(this ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan minimumRemainingLife, int keySize = 256) { var cryptoKeyPair = cryptoKeyStore.GetKeys(bucket).FirstOrDefault(pair => pair.Value.Key.Length == keySize / 8); if (cryptoKeyPair.Value == null || cryptoKeyPair.Value.ExpiresUtc < DateTime.UtcNow + minimumRemainingLife) { ... byte[] secret = GetCryptoRandomData(keySize / 8); DateTime expires = DateTime.UtcNow + SymmetricSecretKeyLifespan; var cryptoKey = new CryptoKey(secret, expires); string handle = GetRandomString(SymmetricSecretHandleLength, Base64WebSafeCharacters); cryptoKeyPair = new KeyValuePair<string, CryptoKey>(handle, cryptoKey); cryptoKeyStore.StoreKey(bucket, handle, cryptoKey); ... } } internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); internal static byte[] GetCryptoRandomData(int length) { byte[] buffer = new byte[length]; CryptoRandomDataGenerator.GetBytes(buffer); return buffer; } internal static string GetRandomString(int length, string allowableCharacters) { char[] randomString = new char[length]; var random = NonCryptoRandomDataGenerator; for (int i = 0; i < length; i++) { randomString[i] = allowableCharacters[random.Next(allowableCharacters.Length)]; } return new string(randomString); }
可以看到,代碼通過判斷bucket參數來確定數據庫存是否存有密鑰.類似於一個分類字段.授權碼的bucket是https://localhost/dnoa/oauth_authorization_code,刷新令牌的是https://localhost/dnoa/oauth_refresh_token.這個是寫死在代碼中的.
handle可以理解為對應的對稱加密的名字.還可以看到,handle是使用System.Random類生成的,密碼雖然也是隨機生成的,但使用的是System.Security.Cryptography.RandomNumberGenerator類,其實現類為System.Security.Cryptography.RNGCryptoServiceProvider.
如果對稱加密密鑰是新生成的,則會將相關信息保存至數據庫.
獲取到對稱加密密鑰后,使用MessagingUtilities類的Encrypt方法加密數據流.
internal static byte[] Encrypt(byte[] buffer, byte[] key) { using (SymmetricAlgorithm crypto = CreateSymmetricAlgorithm(key)) { ... } } private static SymmetricAlgorithm CreateSymmetricAlgorithm(byte[] key) { SymmetricAlgorithm result = null; try { result = new RijndaelManaged(); result.Mode = CipherMode.CBC; result.Key = key; return result; } catch { ... } }
可以看到,對稱加密使用的是SymmetricAlgorithm類.
第23行對加密后的數據進行數字簽名
private byte[] CalculateSignature(byte[] bytesToSign, string symmetricSecretHandle) { if (this.asymmetricSigning != null) { using (var hasher = SHA1.Create()) { return this.asymmetricSigning.SignData(bytesToSign, hasher); } } else { var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle); using (var symmetricHasher = HmacAlgorithms.Create(HmacAlgorithms.HmacSha256, key.Key)) { return symmetricHasher.ComputeHash(bytesToSign); } } }
如果是非對稱加密,則使用Sha1進行簽名,對稱加密則使用Sha256進行簽名.HmacAlgorithms類是作者自行寫的幫助類
internal static class HmacAlgorithms { internal const string HmacSha1 = "HMACSHA1"; internal const string HmacSha256 = "HMACSHA256"; internal const string HmacSha384 = "HMACSHA384"; internal const string HmacSha512 = "HMACSHA512"; internal static HMAC Create(string algorithmName, byte[] key) { Requires.NotNullOrEmpty(algorithmName, "algorithmName"); Requires.NotNull(key, "key"); HMAC hmac = HMAC.Create(algorithmName); try { hmac.Key = key; return hmac; } catch { #if CLR4 hmac.Dispose(); #endif throw; } } }
第26到36行,把可能的數字簽名放在加密流的前面.
第38行將二進制流轉換成Base64字符流.
第40到第43行,如果對稱加密名不為空,且使用了對稱加密或簽名,則將加密名放到字符流前面.
internal static string CombineKeyHandleAndPayload(string handle, string payload) { return handle + "!" + payload; }
反序列化過程大致與之相反
1 public void Deserialize(T message, string value, IProtocolMessage containingMessage, string messagePartName) 2 { 3 string symmetricSecretHandle = null; 4 if (this.encrypted && this.cryptoKeyStore != null) 5 { 6 string valueWithoutHandle; 7 MessagingUtilities.ExtractKeyHandleAndPayload(messagePartName, value, out symmetricSecretHandle, out valueWithoutHandle); 8 value = valueWithoutHandle; 9 } 10 11 message.ContainingMessage = containingMessage; 12 byte[] data = MessagingUtilities.FromBase64WebSafeString(value); 13 14 byte[] signature = null; 15 if (this.signed) 16 { 17 using (var dataStream = new MemoryStream(data)) 18 { 19 var dataReader = new BinaryReader(dataStream); 20 signature = dataReader.ReadBuffer(1024); 21 data = dataReader.ReadBuffer(8 * 1024); 22 } 23 24 // Verify that the verification code was issued by message authorization server. 25 ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature, symmetricSecretHandle), MessagingStrings.SignatureInvalid); 26 } 27 28 if (this.encrypted) 29 { 30 data = this.Decrypt(data, symmetricSecretHandle); 31 } 32 33 if (this.compressed) 34 { 35 data = MessagingUtilities.Decompress(data); 36 } 37 38 this.DeserializeCore(message, data); 39 message.Signature = signature; // TODO: we don't really need this any more, do we? 40 41 if (this.maximumAge.HasValue) 42 { 43 // Has message verification code expired? 44 DateTime expirationDate = message.UtcCreationDate + this.maximumAge.Value; 45 if (expirationDate < DateTime.UtcNow) 46 { 47 throw new ExpiredMessageException(expirationDate, containingMessage); 48 } 49 } 50 51 // Has message verification code already been used to obtain an access/refresh token? 52 if (this.decodeOnceOnly != null) 53 { 54 ErrorUtilities.VerifyInternal(this.maximumAge.HasValue, "Oops! How can we validate a nonce without a maximum message age?"); 55 string context = "{" + GetType().FullName + "}"; 56 if (!this.decodeOnceOnly.StoreNonce(context, Convert.ToBase64String(message.Nonce), message.UtcCreationDate)) 57 { 58 Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", message.Nonce, message.UtcCreationDate); 59 throw new ReplayedMessageException(containingMessage); 60 } 61 } 62 63 ((IMessage)message).EnsureValidMessage(); 64 }
第4到第9行將可能的對稱密碼名稱與Base64加密字符流分開
第12行將Base64字符串還原成二進制流
第15到第26行驗證數字簽名
private bool IsSignatureValid(byte[] signedData, byte[] signature, string symmetricSecretHandle) { if (this.asymmetricSigning != null) { using (var hasher = SHA1.Create()) { return this.asymmetricSigning.VerifyData(signedData, hasher, signature); } } else { return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData, symmetricSecretHandle)); } }
非對稱加密使用固有方法驗證.對於對稱加密,其實就是將獲取加密二進制流重新計算的哈希碼與獲取的哈希碼進行比對.
第28到31行解密二進制流
private byte[] Decrypt(byte[] value, string symmetricSecretHandle) { if (this.asymmetricEncrypting != null) { return this.asymmetricEncrypting.DecryptWithRandomSymmetricKey(value); } else { var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle); ErrorUtilities.VerifyProtocol(key != null, MessagingStrings.MissingDecryptionKeyForHandle, this.cryptoKeyBucket, symmetricSecretHandle); return MessagingUtilities.Decrypt(value, key.Key); } }
如果是非對稱加密,則使用傳入的加密提供者進行解密.如果是對稱加密,則從數據庫中獲取密鑰后進行解密.
第33到36行對數據進行解壓縮
第38行等下再說
第41行到第61行就是進行網絡安全檢查.第一個判斷是指消息過期,消息的生成時間與當前時間的間隔大於設定值.第二個判斷是將客戶端傳來的Nonce值存入數據庫,如果存儲失敗,則說明此值之前使用過,此次請求是偽造的非法請求.這兩者驗證失敗都會引發系統異常.
如果一切成功,則將反序列化的數據返回回去.
在研究最后留下來的SerializeCore方法與DeserializeCore方法前,需要了解框架內另一個概念.作者在框架中自行建立了一套序列化與反序列化系統,目標是將所有實現了IMessage接口的類型序列化成IDirectory<string, string>類型,這類似於一個元數據系統,用來描述每個類,又具備反射的功能,用來操作類的實例.
MessagePart類用來描述對象字段或屬性
internal class MessagePart { ... private PropertyInfo property; private FieldInfo field; private Type memberDeclaredType; internal string GetValue(IMessage message); internal void SetValue(IMessage message, string value); ... }
既然為序列化服務,那么這個類就需要描述序列化的方式,也就是一個從任意類型到字符串的映射.
internal class MessagePart { private static readonly Dictionary<Type, ValueMapping> converters = new Dictionary<Type, ValueMapping>(); private ValueMapping converter; static MessagePart() { ... Map<Uri>(uri => uri.AbsoluteUri, uri => uri.OriginalString, safeUri); Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), null, str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc)); Map<TimeSpan>(ts => ts.ToString(), null, str => TimeSpan.Parse(str)); Map<byte[]>(safeFromByteArray, null, safeToByteArray); Map<bool>(value => value.ToString().ToLowerInvariant(), null, safeBool); Map<CultureInfo>(c => c.Name, null, str => new CultureInfo(str)); Map<CultureInfo[]>(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), null, str => str.Split(',').Select(s => new CultureInfo(s)).ToArray()); Map<Type>(t => t.FullName, null, str => Type.GetType(str)); } private static void Map<T>(Func<T, string> toString, Func<T, string> toOriginalString, Func<string, T> toValue) { Func<object, string> safeToString = obj => obj != null ? toString((T)obj) : null; Func<object, string> safeToOriginalString = obj => obj != null ? toOriginalString((T)obj) : null; Func<string, object> safeToT = str => str != null ? toValue(str) : default(T); converters.Add(typeof(T), new ValueMapping(safeToString, safeToOriginalString, safeToT)); } internal MessagePart(MemberInfo member, MessagePartAttribute attribute) { if (attribute.Encoder == null) { if (!converters.TryGetValue(this.memberDeclaredType, out this.converter)) { this.converter = GetDefaultEncoder(this.memberDeclaredType); } } else { this.converter = new ValueMapping(GetEncoder(attribute.Encoder)); } } private static IMessagePartEncoder GetEncoder(Type messagePartEncoder) { IMessagePartEncoder encoder; lock (encoders) { if (!encoders.TryGetValue(messagePartEncoder, out encoder)) { encoder = encoders[messagePartEncoder] = (IMessagePartEncoder)Activator.CreateInstance(messagePartEncoder); } } return encoder; } }
ValueMapping類負責某對象與字符串之間的轉換.converters字段緩存了各類型與ValueMapping之間的對應關系.在此對象首次加載時就會通過調用Map方法自動注冊常見類型的轉換方式.而構造函數則會從特性中或是從緩存中嘗試獲取ValueMapping.
internal struct ValueMapping { internal readonly Func<object, string> ValueToString; internal readonly Func<string, object> StringToValue; internal ValueMapping(Func<object, string> toString, Func<object, string> toOriginalString, Func<string, object> toValue) : this() { this.ValueToString = toString; this.StringToValue = toValue; } internal ValueMapping(IMessagePartEncoder encoder) : this() { this.ValueToString = obj => (obj != null) ? encoder.Encode(obj) : nullString; this.StringToValue = str => (str != null) ? encoder.Decode(str) : null; } }
這里定義了兩個委托,分別負責對象到字符串和字符串到對象的轉換.如果傳入一個IMessagePartEncoder類型,則將功能委托給此類型執行.
public interface IMessagePartEncoder { string Encode(object value); object Decode(string value); }
由於MessagePartAttribute特性擁有IMessagePartEncoder屬性,這為自定義序列化轉換提供了可能.比如上文曾說的各類令牌的基類AuthorizationDataBag
public abstract class AuthorizationDataBag : DataBag, IAuthorizationDescription { [MessagePart(Encoder = typeof(ScopeEncoder))] public HashSet<string> Scope { get; private set; } }
由於系統未定義從HashSet<string>到字符串之間的轉換,所以需要在標記特性時告知映射類ScopeEncoder
internal class ScopeEncoder : IMessagePartEncoder { public string Encode(object value) { var scopes = (IEnumerable<string>)value; return (scopes != null && scopes.Any()) ? string.Join(" ", scopes.ToArray()) : null; } public object Decode(string value) { return OAuthUtilities.SplitScopes(value); } } public static class OAuthUtilities { public static HashSet<string> SplitScopes(string scope) {var set = new HashSet<string>(scope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries), ScopeStringComparer);return set; } }
可以看到,其實就是HashSet<string>各項用逗號拼接.
上面獲取的映射器最終會在取值或賦值時使用,下面這些賦值或取值的語法非常像.Net中反射的語法.
internal class MessagePart { internal string GetValue(IMessage message) { object value = this.GetValueAsObject(message); return this.ToString(value, false); } private string ToString(object value, bool originalString) { return originalString ? this.converter.ValueToOriginalString(value) : this.converter.ValueToString(value); } internal void SetValue(IMessage message, string value) { this.SetValueAsObject(message, this.ToValue(value)); } private void SetValueAsObject(IMessage message, object value) { if (this.property != null) { this.property.SetValue(message, value, null); } else { this.field.SetValue(message, value); } } private object ToValue(string value) { return this.converter.StringToValue(value); } }
MessageDescription對象用來描述對象
internal class MessageDescription { private Dictionary<string, MessagePart> mapping; internal MessageDescription(Type messageType, Version messageVersion) { this.MessageType = messageType; this.MessageVersion = messageVersion; this.ReflectMessageType(); } private void ReflectMessageType() { this.mapping = new Dictionary<string, MessagePart>(); Type currentType = this.MessageType; do { foreach (MemberInfo member in currentType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { if (member is PropertyInfo || member is FieldInfo) { MessagePartAttribute partAttribute = (from a in member.GetCustomAttributes(typeof(MessagePartAttribute), true).OfType<MessagePartAttribute>() orderby a.MinVersionValue descending where a.MinVersionValue <= this.MessageVersion where a.MaxVersionValue >= this.MessageVersion select a).FirstOrDefault(); if (partAttribute != null) { MessagePart part = new MessagePart(member, partAttribute); if (this.mapping.ContainsKey(part.Name)) { Logger.Messaging.WarnFormat( "Message type {0} has more than one message part named {1}. Inherited members will be hidden.", this.MessageType.Name, part.Name); } else { this.mapping.Add(part.Name, part); } } } } currentType = currentType.BaseType; } while (currentType != null); BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; this.Constructors = this.MessageType.GetConstructors(flags); } }
可以看到,此結象每一個實例都用來描述一個類,在初始化時會調用ReflectMessageType方法.此方法會反射並遍例所有類成員,並將標記了MessagePartAttribute特性的成員對成對應的MessagePart描述類並記錄在對象內部的Dictionary<string, MessagePart>類型字典中.
為了提高性能,作者引入了MessageDescriptionCollection類
internal class MessageDescriptionCollection : IEnumerable<MessageDescription> { private readonly Dictionary<MessageTypeAndVersion, MessageDescription> reflectedMessageTypes = new Dictionary<MessageTypeAndVersion, MessageDescription>(); internal MessageDescription Get(Type messageType, Version messageVersion) { MessageTypeAndVersion key = new MessageTypeAndVersion(messageType, messageVersion); MessageDescription result; lock (this.reflectedMessageTypes) { this.reflectedMessageTypes.TryGetValue(key, out result); } if (result == null) { // Construct the message outside the lock. var newDescription = new MessageDescription(messageType, messageVersion); // Then use the lock again to either acquire what someone else has created in the meantime, or // set and use our own result. lock (this.reflectedMessageTypes) { if (!this.reflectedMessageTypes.TryGetValue(key, out result)) { this.reflectedMessageTypes[key] = result = newDescription; } } } return result; } internal MessageDescription Get(IMessage message); internal MessageDictionary GetAccessor(IMessage message); internal MessageDictionary GetAccessor(IMessage message, bool getOriginalValues) }
可以看到,類內部有個字典,所有新建的MessageDescription對象實例都會被加入其中.而每次從中獲取描述時,如果當前不存在,就會新建一個並返回.
為了能夠在元數據層面操作對象,作者引入了MessageDictionary類
internal class MessageDictionary : IDictionary<string, string> { private readonly IMessage message; private readonly MessageDescription description; private readonly bool getOriginalValues; internal MessageDictionary(IMessage message, MessageDescription description, bool getOriginalValues) { this.message = message; this.description = description; this.getOriginalValues = getOriginalValues; } public void Add(string key, string value) { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { if (part.IsNondefaultValueSet(this.message)) { throw new ArgumentException(MessagingStrings.KeyAlreadyExists); } part.SetValue(this.message, value); } else { this.message.ExtraData.Add(key, value); } } }
可以看到,每一個MessageDictionary類實例都與一個IMessage對象實例和一個MessageDescription對象實例關聯,在構造函數中傳入.對MessageDictionary的操作就是對IMessage的操作.
比如上面所舉的Add方法,首先去MessageDescription處獲取名為key的MessagePart對象.如果獲取不到,說明要么IMessage對象沒有此屬性或字段,要么沒有標記MessagePartAttribute特性導致沒有對應的MessagePart描述對象.於是將其存於ExtraData屬性中.此屬性是IDictionary<string, string>類型.如果獲取到了,則查看IMessage對象的此成員當前是否是默認值,如果不是默認值,則說明之前已賦過值,現在屬於重復賦值,於是拋出異常,否則使用MessagePart對象的SetValue方法對其賦值.這里的賦值就用到了上面提的映射器.
最后,使用MessageSerializer類將MessageDictionary類實例序列化成IDictionary<string, string>,或將IDictionary<string, string>反序列化成MessageDictionary
internal class MessageSerializer { private readonly Type messageType; private MessageSerializer(Type messageType) { this.messageType = messageType; } internal static MessageSerializer Get(Type messageType) { return new MessageSerializer(messageType); } internal IDictionary<string, string> Serialize(MessageDictionary messageDictionary) { var result = new Dictionary<string, string>(); foreach (var pair in messageDictionary) { MessagePart partDescription; if (messageDictionary.Description.Mapping.TryGetValue(pair.Key, out partDescription)) { Contract.Assume(partDescription != null); if (partDescription.IsRequired || partDescription.IsNondefaultValueSet(messageDictionary.Message)) { result.Add(pair.Key, pair.Value); } } else { // This is extra data. We always write it out. result.Add(pair.Key, pair.Value); } } return result; } internal void Deserialize(IDictionary<string, string> fields, MessageDictionary messageDictionary) { foreach (var pair in fields) { messageDictionary[pair.Key] = pair.Value; } } }
對於序列化,直接遍歷MessageDictionary類,將必填成員,非默認值成員和非MessageDescription類描述的成員都取出來,組裝成Dictionary<string, string>返回.對於反序列化,則是遍歷IDictionary<string, string>,將值取出裝入MessageDictionary類並返回.
最后再來研究SerializeCore方法與DeserializeCore方法
SerializeCore方法如下
protected override byte[] SerializeCore(T message) { var fields = MessageSerializer.Get(message.GetType()).Serialize(MessageDescriptions.GetAccessor(message)); string value = MessagingUtilities.CreateQueryString(fields); return Encoding.UTF8.GetBytes(value); }
第一行,將信息通過上面所述的方式序列化成IDictionary<string, string>類型,然后將其轉換成QueryString字符串
internal static string CreateQueryString(IEnumerable<KeyValuePair<string, string>> args) { StringBuilder sb = new StringBuilder(args.Count() * 10); foreach (var p in args) { sb.Append(EscapeUriDataStringRfc3986(p.Key)); sb.Append('='); sb.Append(EscapeUriDataStringRfc3986(p.Value)); sb.Append('&'); } sb.Length--; // remove trailing & return sb.ToString(); }
可以看到,很簡單,就是遍歷字值對拼接字符串,,最后使用Utf8進行二進制編碼后返回.
DeserializeCore方法如下
protected override void DeserializeCore(T message, byte[] data) { string value = Encoding.UTF8.GetString(data); // Deserialize into message newly created instance. var serializer = MessageSerializer.Get(message.GetType()); var fields = MessageDescriptions.GetAccessor(message); serializer.Deserialize(HttpUtility.ParseQueryString(value).ToDictionary(), fields); }
首先將二進制流轉換成Utf8編碼的字符串,然后將此串轉換成QueryString字符串,然后再次轉換成IDictionary<string, string>字典
internal static Dictionary<string, string> ToDictionary(this NameValueCollection nvc) { return ToDictionary(nvc, false); } internal static Dictionary<string, string> ToDictionary(this NameValueCollection nvc, bool throwOnNullKey) { var dictionary = new Dictionary<string, string>(); foreach (string key in nvc) { dictionary.Add(key, nvc[key]); } return dictionary; }
最后反序列化入指定類型的MessageDescriptions類中並返回.
3.資源服務端
在.Net中,有兩個類別的體系來保證安全,通過代碼訪問安全,使代碼可以根據它所來自的位置以及代碼標識的其他方面,獲得不同等級的受信度,減小惡意代碼或包含錯誤的代碼執行的可能性,來保證二進制層面的執行安全.比如在沙箱中運行的程序,就是部分信任程序,比如無法操作本地硬盤文件.通過基於角色的安全,使代碼判定當前用戶是誰以及擁有的角色,獲取不同的權限,來保證業務安全.DotNetOpenAuth框架使用的是后者.
參考:
在基於角色的安全中,有兩個重要的概念及其對應的接口:標識與主體.
.Net使用標識來表達用戶,其接口定義如下:
public interface IIdentity { string AuthenticationType { get; } bool IsAuthenticated { get; } string Name { get; } }
最重要的就是用戶名Name和是否通過驗證IsAuthenticated.
.Net使用主體來表達安全上下文
public interface IPrincipal { IIdentity Identity { get; } bool IsInRole(string role); }
其包括當前環境的用戶與判斷用戶是否屬於某接口.
在不用的應用程序執行上下文中都可以通過獲取主體接口來判斷當前用戶授權與驗證信息.在Windowns程序中,通過
System.Threading.Thread.CurrentPrincipal
獲取,在Asp.Net中,通過
System.Web.HttpContext.Current.User
獲取.
DotNetOpenAuth在資源服務端的作用就是確定請求身份.其編程的核心對象為ResourceServer,通過GetPrincipal方法便可獲取請求的主體.
public class ResourceServer { public IAccessTokenAnalyzer AccessTokenAnalyzer { get; private set; } public ResourceServer(IAccessTokenAnalyzer accessTokenAnalyzer) { this.AccessTokenAnalyzer = accessTokenAnalyzer; } public virtual IPrincipal GetPrincipal(HttpRequestBase httpRequestInfo = null, params string[] requiredScopes) { AccessToken accessToken = this.GetAccessToken(httpRequestInfo, requiredScopes); string principalUserName = !string.IsNullOrEmpty(accessToken.User) ? this.ResourceOwnerPrincipalPrefix + accessToken.User : this.ClientPrincipalPrefix + accessToken.ClientIdentifier; string[] principalScope = accessToken.Scope != null ? accessToken.Scope.ToArray() : new string[0]; var principal = new OAuthPrincipal(principalUserName, principalScope); return principal; } public virtual AccessToken GetAccessToken(HttpRequestBase httpRequestInfo = null, params string[] requiredScopes) { accessToken = this.AccessTokenAnalyzer.DeserializeAccessToken(request, request.AccessToken); } }
可以看到,程序通過IAccessTokenAnalyzer接口獲取訪問令牌,實際的實現類為StandardAccessTokenAnalyzer
public class StandardAccessTokenAnalyzer : IAccessTokenAnalyzer { public StandardAccessTokenAnalyzer(RSACryptoServiceProvider authorizationServerPublicSigningKey, RSACryptoServiceProvider resourceServerPrivateEncryptionKey) { this.AuthorizationServerPublicSigningKey = authorizationServerPublicSigningKey; this.ResourceServerPrivateEncryptionKey = resourceServerPrivateEncryptionKey; } public RSACryptoServiceProvider AuthorizationServerPublicSigningKey { get; private set; } public RSACryptoServiceProvider ResourceServerPrivateEncryptionKey { get; private set; } public virtual AccessToken DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken) { var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey); accessTokenFormatter.Deserialize(token, accessToken, message, Protocol.access_token); return token; } }
很簡單,就是通過上文所說的將請求中特定信息反序列化為訪問令牌.
實際編程中只需使用上面兩個類就可以獲取應用程序所需的主體
private static IPrincipal VerifyOAuth2(HttpRequestMessageProperty httpDetails, Uri requestUri, params string[] requiredScopes) { // for this sample where the auth server and resource server are the same site, // we use the same public/private key. using (RSACryptoServiceProvider authorizationRas = GetAuthorizationServerRsa()) { using (RSACryptoServiceProvider resourceRas = GetResourceServerRsa()) { var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(authorizationRas, resourceRas)); return resourceServer.GetPrincipal(httpDetails, requestUri, requiredScopes); } } }
上面這段程序截取自Sample,方法參數與編程環境有關.這個是為Wcf獲取驗證主體.
此主體是DotNetOpenAuth框架對IPrincipal的實現實:OAuthPrincipal,從上面的GetPrincipal方法可以看出,主體的用戶名是訪問令牌用戶名,角色集合為權限范圍
public class OAuthPrincipal : IPrincipal { private ICollection<string> roles; public OAuthPrincipal(string userName, string[] roles) : this(new OAuthIdentity(userName), roles) { } internal OAuthPrincipal(OAuthIdentity identity, string[] roles) { this.Identity = identity; this.roles = roles; } public string AccessToken { get; protected set; } public ReadOnlyCollection<string> Roles { get { return new ReadOnlyCollection<string>(this.roles.ToList()); } } public IIdentity Identity { get; private set; } public bool IsInRole(string role) { return this.roles.Contains(role, StringComparer.OrdinalIgnoreCase); } public GenericPrincipal CreateGenericPrincipal() { return new GenericPrincipal(new GenericIdentity(this.Identity.Name), this.roles.ToArray()); } }
OAuthIdentity類是框架對IIdentity接口的實現
public class OAuthIdentity : IIdentity { internal OAuthIdentity(string username) { Requires.NotNullOrEmpty(username, "username"); this.Name = username; } public string AuthenticationType { get { return "OAuth"; } } public bool IsAuthenticated { get { return true; } } public string Name { get; private set; } }
很簡單,就不多說了.
六.小結
就我個人而言,雖然此框架功能強大,但感覺寫的過於復雜,有很多處理細節與意圖都掩埋於代碼之后,有時會出現一些莫明奇妙的處理.希望之后的版本能有所改善.
示例項目下載
參考:
The OAuth 2.0 Authorization Framework
OAuth 2.0 Threat Model and Security Considerations