基於OWIN+DotNetOpenOAuth實現OAuth2.0


這幾天時間一直在研究怎么實現自己的OAuth2服務器,對於太了解OAuth原理以及想自己從零開始實現的,我建議可以參考《Apress.Pro ASP.NET Web API Security》里面的章節。最后發現其實微軟在這方面也已經做了實現,所以文介紹下怎么基於OWIN來實現自己的OAuth2.0授權服務器,,以及怎么使用DotNetOpenAuth作為客戶端來訪問受保護的資源。  OWIN是一套specification,微軟的Katana開源項目是基於OWIN標准開發的,所以本本文更准確的說是用katana來實現OAuth2.0授權服務器。DotNetOpenAuth也是一個非常優秀的框架,它也可以實現OAuth2.0服務器,本文是用這個框架來做客戶端。更多的關於Owin, OAuth等標准和框架,不是本文的重點,您可以自己查閱一些相關資料。本文假設您已經對OAuth,OWIN已經有一定的了解。

 

利用微軟的OWIN搭建OAuth非常簡單,具體步驟如下:

  1. 先創建一個ASP.NET項目,最簡單直接的的方式是創建一個MVC項目並且在身份驗證時選擇Individual User Accounts,這樣會自動將OWIN相關的DLL加入到項目
  2. 接下來找到App_Start文件夾下面的Startup.Auth.cs文件,加入如下代碼:(不知道為什么插入代碼的時候我不能折疊:( )
                //創建OAuth授權服務器
                app.UseOAuthAuthorizationServer(new Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerOptions()
                {
                    AuthorizeEndpointPath = new PathString("/OAuth/Authorize"),
                    TokenEndpointPath = new PathString("/OAuth/Token"),
                    AccessTokenExpireTimeSpan=TimeSpan.FromMinutes(1),
                    Provider = new OAuthAuthorizationServerProvider()
                    {
                        OnValidateClientRedirectUri = context =>
                        {
                            context.Validated();
                            return Task.FromResult(0);
                        },
                        
                        OnValidateClientAuthentication = context =>
                        {
                            string clientId;
                            string clientSecret;
                            if (context.TryGetBasicCredentials(out clientId, out clientSecret) ||
                                  context.TryGetFormCredentials(out clientId, out clientSecret))
                            context.Validated();
    
                            return Task.FromResult(0);
                        }
    
    
    
                    },
    
                    AuthorizationCodeProvider = new AuthenticationTokenProvider()
                    {
                        OnCreate = context =>
                        {
                            context.SetToken(DateTime.Now.Ticks.ToString());
                            string token = context.Token;
                            string ticket = context.SerializeTicket();
                            _authenticationCodes[token] = ticket;
                        },
    
                        OnReceive = context =>
                        {
                            string token = context.Token;
                            string ticket;
                            if (_authenticationCodes.TryRemove(token, out ticket))
                            {
                                context.DeserializeTicket(ticket);
                            }
                        },
    
                    },
    
                    RefreshTokenProvider = new AuthenticationTokenProvider() 
                    {
                        OnCreate = context =>
                        {
                            context.SetToken(context.SerializeTicket());
                        },
    
                        OnReceive = context =>
                        {
                            context.DeserializeTicket(context.Token);
                        },
                    }
    
    
    
    
                });

    其中AuthorizeEndpointPath是授權終結點,這個需要自己去寫實現邏輯,等會會介紹。TokenEndpointPath是生成Token的終結點,這個不需要我們寫額外的邏輯了,因為Token的生成涉及到很多方面,例如序列化反序列加密解密等邏輯,所以框架默認已經幫我們做好了。 Provider里面是用來驗證客戶端跳轉地址和客戶端驗證,我這里實現比較簡單,全部都驗證通過,正確的做法應該是先判斷客戶端發送過來的ClientID是否合法,如果合法則驗證通過。AuthorizationCodeProvider是對於Authorize Code模式而言的(我等會客戶端訪問只接受這種模式,也是最負責的一種,其他三種模式Implicit,Client,Resource相對比較簡單,就不在這里介紹了,有興趣的同學可以線下聯系),里面的兩個Action委托顧名思義一個是創建Code,一個是用Code來來交換Token。最后是RefreshTokenProvider

  3. 接下來就看下授權終結點,這里的邏輯需要自己來處理
     public ActionResult Authorize()
            {           
    
                IAuthenticationManager authentication= HttpContext.GetOwinContext().Authentication;
                AuthenticateResult ticket = authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie).Result;
                ClaimsIdentity identity = ticket == null ? null : ticket.Identity;
            
                if (identity == null)
                {
                    //如果沒有驗證通過,則必須先通過身份驗證,跳轉到驗證方法
                    authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie);
                    return new HttpUnauthorizedResult();
                }
    
                
                identity = new ClaimsIdentity(identity.Claims, "Bearer");
                //hardcode添加一些Claim,正常是從數據庫中根據用戶ID來查找添加
                identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
                identity.AddClaim(new Claim(ClaimTypes.Role, "Normal"));
                identity.AddClaim(new Claim("MyType", "MyValue"));
                
                authentication.SignIn(new AuthenticationProperties() { IsPersistent=true }, identity);
                       
    
                return View();
            }

    通常來說,客戶端發送到授權終結點的請求是會被OWIN截取跳轉到第二步中注冊的 OnValidateClientRedirectUri委托,如果驗證通過再跳轉到授權終結點,這里首先看當前用戶是否已經驗證通過(Authentication),如果沒有則跳轉到驗證終結點,注意這里的方法authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie)中的參數,名稱必須要和Startup.Auth.cs中app.UseCookieAuthentication內定義的AuthenticationType一樣,否則無法跳轉到在app.UseCookieAuthentication內注冊的LoginPath終結點

  4. 驗證節點(Authentication)代碼
            [HttpPost]
            [AllowAnonymous]
            [ValidateAntiForgeryToken]
            public ActionResult Login(LoginViewModel model, string returnUrl)
            {
                if (!ModelState.IsValid)
                {
                    return View(model);
                }
    
    
    
                if (_userManagerService.VerifyUser(model.Email, model.Password))
                {
                    AppUser user = UserManager.FindByName(model.Email);
                    //可以在這里將用戶所屬的role或者Claim添加到此
                    ClaimsIdentity claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.UserName)
                    ,new Claim(ClaimTypes.NameIdentifier,user.Id)
                    ,new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",user.Id)},
                        DefaultAuthenticationTypes.ApplicationCookie);
    
                    AuthenticationProperties properties = new AuthenticationProperties
                    {
                        IsPersistent = true
                    };
    
                    ClaimsPrincipal principal = new ClaimsPrincipal(claims);
                    //System.Threading.Thread.CurrentPrincipal = principal;
                    this.AuthenticationManager.SignIn(properties, new[] { claims });
    
    
                    return RedirectToLocal(returnUrl);
                }
                else
                {
                    ModelState.AddModelError("", "Invalid login attempt.");
                    return View(model);
                }
        }

    通過第三步首先會進入到Login的Get方法(這里沒有貼出來,因為只是返回試圖而已),然后用戶通過輸入正確的用戶名和密碼通過驗證后,再為用戶添加代碼中所示的Claim,注意如果你的代碼中加入了[ValidateAntiForgeryToken]特性,那么需要加入NameIdentifier,identityprovider兩個聲明,否則會報錯。最后再調用SignIn方法(我沒有仔細去看這個方法的代碼,但是我認為這里應該至少會有對System.Threading.Thread.CurrentPrincipal進行賦值,確保驗證通過)然后再次跳轉到授權節點,再次執行第三步的代碼,這是identity就不為空了,我這里重新構造了Identity,注意這里的AuthenticationType需要為“Bearer”,這是告訴OAuth生成Token的關鍵。最后再進行SignIn,此時會執行第二步中注冊的Authorize Code生成方法,生成Code后會回跳到客戶端的注冊的RedirectUrl,客戶端此時就可以拿着Code來換Token了,具體也就是執行第二步中注冊的OnReceive方法

  5. 好了,整個服務器授權邏輯差不多就是這樣了,當然我這里為了簡單沒有去實現Scope的Grant,如果要做可以在授權終結點里進行實現
  6. 下面來看看客戶端怎么實現的,首選需要說明一點如果是.NET的WEB客戶端可以使用DotNetOpenOAuth,通過Nuget就可以獲取,比較簡單,如果是其他手機設備等則可以通過ImplicitGrant模式通過javascript腳本來做,implicitgrant是authorize code的而一個簡化版,省略了code交互這些步驟,直接從服務器拿Token,而且客戶端是能直接看到Token的,但是authorize code客戶端是看不到的,只有客戶端所在的web server才能看到,所以也就更安全。
  7. 我在客戶端自己稍微對DNOA做了下封裝,代碼有點長,所以就只看下幾個核心方法
            public string[] scopes { get; set; }
            public string redirectUri { get; set; }
       public IAuthorizationState AuthorizationState = null;
            private string AuthorizationEndpoint, TokenEndpoint, ClientId, ClientKey;
            private AuthorizationServerDescription authServer = null;
    
         
            private WebServerClient oauthWebClient = null; 
            public OAuthClient()
            {
             
                AuthorizationEndpoint = System.Configuration.ConfigurationManager.AppSettings.Get("AuthorizeEndpoint");
                TokenEndpoint = System.Configuration.ConfigurationManager.AppSettings.Get("TokenEndpoint");
                ClientKey = System.Configuration.ConfigurationManager.AppSettings.Get("ClientKey");
                ClientId = System.Configuration.ConfigurationManager.AppSettings.Get("ClientID");
                authServer = new AuthorizationServerDescription
                {
                    AuthorizationEndpoint = new Uri(AuthorizationEndpoint),
                    TokenEndpoint = new Uri(TokenEndpoint)
                };
    
             
                oauthWebClient = new WebServerClient(authServer, clientIdentifier: ClientId, clientSecret:ClientKey);
                AuthorizationState = new AuthorizationState();
              
            }

     

    比較重要的就是WebServerClient這個對象了,封裝了對授權服務器訪問的所有方法,對它初始化需要告訴它的授權和Token終結點,ClientID,ClientSecret(這兩個值正常做法是授權服務器頒給的,和新浪,騰訊等一樣)

  8. 初始化好了后就可以開始訪問了,因為我們是使用authorize code,所以這里調用的 
     oauthWebClient.RequestUserAuthorization(scopes, new Uri(redirectUri));

    傳入你要訪問資源的范圍和回調的URL(通常就是發起的action),獲得Code后就可以服務器會把code值作為參數寫入到Request的querystring中,所以我們可以下面方式來檢查code是否已經存在

    if (request.Url.AbsoluteUri.Contains(redirectUri))
                {
                    if (!string.IsNullOrEmpty(request.Params["code"]))
                    {
                        return true;
                    }
                }

    如果code存在就可以用code來換Token了,馬上就會大功告成了。。

    public void GetAccessToken(HttpRequestBase Requst, out string token)
            {
                if (CheckCodeExist(Requst))
                {
                    AuthorizationState = oauthWebClient.ProcessUserAuthorization();
                    if (AuthorizationState != null)
                    {
                        if (!string.IsNullOrEmpty(AuthorizationState.AccessToken))
                        {
                           
    
                            token = AuthorizationState.AccessToken;
    
    
                            return;
                        }
                    }
                }
               GetAuthrizationCode();           
               token = null;
    
            }

    最關鍵的方法就是AuthorizationState = oauthWebClient.ProcessUserAuthorization();通過它我們就可以拿到token相關的信息了,例如AccessToken, RefreshToken,Token過期時間等

  9. 接下來就是用Token去訪問資源服務器上的資源了,訪問之前,我們先要對資源服務器同樣要加入OWIN的支持,由於我采用的IISHost,需要加入下列組件microsoft.owin,micorsoft.owin.host.systemweb, microsoft.owin.security,microsoft.owin.oauth,通過Nuget獲取既可,不做多介紹,這時應該會有Startup.cs文件自動加進來,我們在里面加上下面代碼:
     public partial class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());
            }
        }

    這里就是使用Bearer驗證,具體邏輯是拿到請求頭里的Bearer驗證帶來的Token,如果token是合法的,那么OWIN對token進行解析反序列化出來對等的claim,並且通過驗證,還可以進一步根據claim來判斷當前用戶是否有權限來相應的資源,如下(用戶必須是Admin角色,否則會401):

            [Authorize]
            [HttpGet]
            [Route("api/car/protect")]
            public string ProtectedResource()
            {
                ClaimsPrincipal principal = Thread.CurrentPrincipal as ClaimsPrincipal;
                var isInRole= principal.IsInRole("Admin");
                return "you have the right now to access me!";
                 
            }

    另外需要注意點的是,如果你需要實現單點登錄之類的功能或者是訪問多台資源服務器,那么需要保證不同的資源服務器使用的都是相同加密解密機制,這個可以通過在web.config里設置machiekey來實現,關於machine key這里就不說了

  10. 回到剛才9步中說到的客戶端訪問資源,很簡單,我是通過httpclient做了個簡單的訪問實現,具體如下,關鍵就是將token以Bearer方式添加到驗證頭,就不多復述了
                string msg="";
                using (HttpClient httpClient = new HttpClient())
                {
                    httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
                   httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
                    if (method.ToUpper() == "POST")
                    {
                        StringContent content = new StringContent(jsonStr);
                        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
                        foreach (var address in resourceAddress)
                        {
                            var responseMsg = httpClient.PostAsync(address, content).Result;
                            if (responseMsg.IsSuccessStatusCode)
                            {
                                msg += responseMsg.Content.ReadAsStringAsync().Result; ;
                            }
                        }
    
                    }
                    else
                    {
                        foreach (var address in resourceAddress)
                        {
                            var responseMsg = httpClient.GetAsync(address).Result;
                            if (responseMsg.IsSuccessStatusCode)
                            {
                                msg += responseMsg.Content.ReadAsStringAsync().Result; ;
                            }
                        }
                    }

     

現在整個授權服務器,資源服務器,客戶端基本都實現了。

 

第一次寫博,原以為最多一個小時可以搞定,沒想到快四個小時了,所以沒時間來檢查以及弄排版樣式啥的了,寫博客還真不是容易的事啊。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM