在上篇文章中我研究了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