在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在上篇文章中我研究了OpenId及DotNetOpenAuth的相关应用,这一篇继续研究OAuth2. https://github.com/DotNetOpenAuth http://www.cnblogs.com/ljzforever/archive/2013/04/01/2985456.html 一.什么是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) { ... 可以看到定义了两个集合分别存储请求过滤器与响应过滤器.两者都由构造函数初始化,内容一样,顺序相反.在读取请求时会遍历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放在一起,可以表示服务器在某个时间点生成的随机数。这样就算生成的随机数相同,但因为它们生成的时间点不一样,所以也算有效的随机数 |
请发表评论