[原]ASP.NET MVC 3 使用 DotNetOpenAuth 實現SSO

來源:互聯網
上載者:User

轉載註明作者及出處,謝謝

聽到DotNetOpenAuth是去年某一天的事了,當時在讀《RESTful Web Service》時突然好像靈光一閃,覺得Authorization這個問題似乎應該在構建服務之前就先考慮清楚,否則服務化似乎就無從談起了。為什麼這麼說呢,舉例來說,Google Canlendar是一個服務,你現在使用Google Canlendar又構建了另一個服務,並幸運的擁有了一些使用者,但這些使用者怎麼才能放心的把Google的帳戶資訊交給你,讓你拿去Google驗證呢;另一點,我們公司現在項目比較多,每個新項目建立後,都要往裡複製一份諸如Organization,UserManager之內的公用檔案(主要集中在UI層),增加工作量不說,這些不同拷貝的版本更新就是一個嚴峻的挑戰,更不要說如果一個客戶同時使用了我們兩個產品,就會發現居然同一個人要維護和軟體產品數量相同的使用者...這一切,使得將Organization之類的組件成為服務的要求變得非常強烈,即把Organization業務本身做為一個應用程式存在,發布在IIS後其他項目使用其提供的資料服務即可,這樣就不存在每次都要複製UI,以及多個產品間多套資料的問題了。到這裡,使用SSO似乎已經不可避免了,但是有個問題還沒有考慮到,在使用Web Service時不會總是直接使用頁面去調用吧,大多數時候得提供一個服務用戶端組件,否則有誰會每次調用你一個資料,還在業務層裡添加一堆有關Authorization和Cookie的代碼?所以看起來使用簡單的SSO實現方式,很難高效,體面的解決問題。

於是我就發現居然有DotNetOpenAuth(真不明白為什麼被牆了,想看的翻牆吧,源碼託管地址)這麼個東西了,看介紹似乎就涵蓋了我想要的一切功能!

文檔可能是最讓人又愛又恨的東西了,自已不願意寫文檔,但都希望使用別人的東西時有完備的文檔。

前兩天在學習jqGrid,沒有一個像樣的文檔就灰常痛苦,一天下來,瀏覽歷史裡就只有Google的搜尋記錄了。當時下載DotNetOpenAuth下來也是一樣,除了一個API Documents之外,沒有任何有點價值的資料,雖然他們提供了好幾個範例,但是對於一個比較複雜的技術來說,這些遠遠不夠,最起碼得有個Quick Start或是How to吧,可惜官網上幾乎啥也沒有。也不知到底是啥原因我後來也沒有再去看這個組件。

一直到前兩天,我還是覺得如果說要考慮無論是Web Service,抑或SOA實踐,乃至現在火熱的雲端運算,如果Authorization問題不解決掉,似乎就無從談起。很多的書上使用大量的篇幅講解如何設計,實現一個Service,但卻很少提及SOA實踐或雲端運算的實務講解,結果大家一通倒騰之後,一個個所謂的Web Service拔地而起,但怎麼看也不像是“雲”。解決Authorization問題,還是從DotNetOpenAuth入手比較好,它功能強大,而且oauth和openid是成熟的產品,使用的公司很多,幾乎成了事實標準,找一個和當前工作比較貼近的點,就學習下OpenIdSSOProvider吧。

(這篇文章呢,我想來想去不知該如何去寫,為什麼呢?主要是我認為SOA或是雲端運算是一個非常飄渺的東西,惡補一段時間後,我總會覺得對其概念還算清楚,但是時間一長就會又模糊不清,寫自已都不太清楚的東西,遭人罵是小事,耽誤人是大事。我本是個看貼不回貼的人,但是現在網上有關服務設計實務的東西少之有少,DotNetOpenAuth方面的東西也是鳳毛麟角,我是不想回貼,但是看貼也沒得看,因此權當拋磚引玉,希望能和我有共同想法的人探討一二)

說了這麼多廢話後,進入正題...

DotNetOpenAuth本身提供了一SSOProvider樣本,但是只有WebForm項目的,沒有MVC的SSOProvider樣本,本文提供MVC的SSOProvider實現方法,再順便講講個人對於使用DotNetOpenAuth的一點點小體會。

一.SsoOP SSO的服務提供者

1.建立SsoOP項目,我使用了Razor視圖引擎,添加DotNetOpenAuth.dll引用。見上面的源碼託管地址.

2.設定web.config檔案裡面的配置資訊,詳情請見本文下方樣本程式。

3.建立OpenIdController.cs

public class OpenIdController : Controller    {        internal static OpenIdProvider openIdProvider = new OpenIdProvider();        public ActionResult Identifier()        {            if (User.Identity.IsAuthenticated && ProviderEndpoint.PendingAuthenticationRequest != null)            {                Util.ProcessAuthenticationChallenge(ProviderEndpoint.PendingAuthenticationRequest);                if (ProviderEndpoint.PendingAuthenticationRequest.IsAuthenticated.HasValue)                {                    ProviderEndpoint.SendResponse();                }            }            if (Request.AcceptTypes.Contains("application/xrds+xml"))            {                return new TransferResult("~/OpenId/Xrds");            }            return View();        }        [ValidateInput(false)]        public ActionResult Provider()        {            var request = openIdProvider.GetRequest();            if (request != null)            {                if (request.IsResponseReady)                {                    return openIdProvider.PrepareResponse(request).AsActionResult();                }                ProviderEndpoint.PendingRequest = (IHostProcessedRequest)request;                var idrequest = request as IAuthenticationRequest;                return Util.ProcessAuthenticationChallenge(idrequest);            }            return View();        }        public ActionResult AskUser()        {            return View();        }        public ActionResult Xrds()        {            return View();        }    }

OpenIdController是SsoRP(SSO消費者)使用OP的進入點,其中Provider是提供登入服務的Action,這點需要在後面提到。

4.建立Xrds.cshtml視圖

@{    Layout = null;    Response.ContentType = "application/xrds+xml";    var uri = new Uri(Request.Url, Response.ApplyAppPathModifier("~/OpenId/Provider")).ToString();}<?xml version="1.0" encoding="UTF-8"?><xrds:XRDSxmlns:xrds="xri://$xrds"xmlns:openid="http://openid.net/xmlns/1.0"xmlns="xri://$xrd*($v*2.0)"><XRD><Service priority="10"><Type>http://specs.openid.net/auth/2.0/server</Type><Type>http://openid.net/extensions/sreg/1.1</Type><URI>@uri</URI></Service></XRD></xrds:XRDS>

本視圖的用法也將在以後提到。

5.建立AskUser.cshtml視圖

@{    Layout = null;    Response.ContentType = "application/xrds+xml";    var uri1 = new Uri(Request.Url, Response.ApplyAppPathModifier("~/OpenId/Provider")).ToString();    var uri2 = new Uri(Request.Url, Response.ApplyAppPathModifier("~/OpenId/Provider")).ToString();}<?xml version="1.0" encoding="UTF-8"?><xrds:XRDSxmlns:xrds="xri://$xrds"xmlns:openid="http://openid.net/xmlns/1.0"xmlns="xri://$xrd*($v*2.0)"><XRD><Service priority="10"><Type>http://specs.openid.net/auth/2.0/signon</Type><Type>http://openid.net/extensions/sreg/1.1</Type><URI>@uri1</URI></Service><Service priority="20"><Type>http://openid.net/signon/1.0</Type><Type>http://openid.net/extensions/sreg/1.1</Type><URI>@uri2</URI></Service></XRD></xrds:XRDS>

對於本Action的作用,現在還很含糊,只能大概的猜測其意圖,其用法也將在以後提到。

6.建立TransferResult類,什麼作用呢,這裡稍作解釋:在ASP.NET WebForm頁面中,有人可能用過Server.Transfer方法,該方法MSDN中的解釋是:對於當前請求,終止當前頁的執行,並使用指向一個新頁的指定 URL 路徑來開始執行此新頁。一般情況下似乎和Redirect方法的作用很像,但是某些特殊場合中,區別是大大的,是什麼呢?Redirect是執行用戶端重新導向,而Transfer是不用用戶端重新導向的,應該就是HTTP的302狀態吧。在使用DotNetOpenAuth的過程中,很多時候也許是基於安全的考慮,OpenId是不允許使用重新導向了的請求,不然就會出錯。在MVC中有一個RedirectToAction方法很好用,卻沒有一個TransferToAction方法,甚至沒有TransferResult類型,所以不得不自已弄一個。

    public class TransferResult : RedirectResult    {        public TransferResult(string url)            : base(url)        {        }        public override void ExecuteResult(ControllerContext context)        {            var httpContext = HttpContext.Current;            httpContext.RewritePath(Url, false);            IHttpHandler httpHandler = new MvcHttpHandler();            httpHandler.ProcessRequest(HttpContext.Current);        }    }

7.其他的代碼,限於篇幅,就不一一貼上來了,全放到樣本程式裡面,結構如下:

/Code/ReadOnlyXmlMembershipProvider.cs    作用:使用者驗證

/Code/Util.cs    作用:用於處理登入及許可權請求,這個類裡面的主要方法為:ProcessAuthenticationChallenge,在官方提供的範例中是一個void,用在MVC中,必須使用一個具有ActionResult傳回值的方法了。

/AppData/Users.xml    作用:相當於存使用者資訊的資料庫

8.在項目根目錄下建立default.aspx,該檔案為使用IIS架設程式時的入口

<%@ Page Language="C#" AutoEventWireup="true" %><script runat="server">protected void Page_Load(object sender, EventArgs e) {Response.Redirect("~/Home/Index");}</script>

OK,SsoOP主要結構就是上面這些,文檔結構見(其中選中的檔案是新增的,其他的都是項目模板內建的):

二.SsoRP 這個RP和人品沒有太大關係,作用為SSO的消費者

文檔結構如下:

這個項目主要內容如下:

1.將AccountController類中的內容全部注釋,添加以下代碼:

    public class AccountController : Controller    {        private const string RolesAttribute = "http://samples.dotnetopenauth.net/sso/roles";        private static OpenIdRelyingParty relyingParty = new OpenIdRelyingParty();        public ActionResult LogOn()        {            if (Array.IndexOf(Request.AcceptTypes, "application/xrds+xml") >= 0)            {                return View("Xrds");            }                        UriBuilder returnToBuilder = new UriBuilder(Request.Url);            returnToBuilder.Path = "/Account/LogOn";            returnToBuilder.Query = null;            returnToBuilder.Fragment = null;            Uri returnTo = returnToBuilder.Uri;            returnToBuilder.Path = "/Account/LogOn";            Realm realm = returnToBuilder.Uri;            var response = relyingParty.GetResponse();            if (response == null)            {                if (Request.QueryString["ReturnUrl"] != null && User.Identity.IsAuthenticated)                {                    // The user must have been directed here because he has insufficient                    // permissions to access something.                    this.ViewBag.Message = "1";                }                else                {                    // Because this is a sample of a controlled SSO environment,                    // we don't ask the user which Provider to use... we just send                    // them straight off to the one Provider we trust.                    var request = relyingParty.CreateRequest(                        ConfigurationManager.AppSettings["SsoProviderOPIdentifier"],                        realm,                        returnTo);                    var fetchRequest = new FetchRequest();                    fetchRequest.Attributes.AddOptional(RolesAttribute);                    request.AddExtension(fetchRequest);                    request.RedirectToProvider();                }            }            else            {                switch (response.Status)                {                    case AuthenticationStatus.Canceled:                        this.ViewBag.Message = "Login canceled.";                        break;                    case AuthenticationStatus.Failed:                        this.ViewBag.Message = HttpUtility.HtmlEncode(response.Exception.Message);                        break;                    case AuthenticationStatus.Authenticated:                        IList<string> roles = null;                        var fetchResponse = response.GetExtension<FetchResponse>();                        if (fetchResponse != null)                        {                            if (fetchResponse.Attributes.Contains(RolesAttribute))                            {                                roles = fetchResponse.Attributes[RolesAttribute].Values;                            }                        }                        if (roles == null)                        {                            roles = new List<string>(0);                        }                        // Apply the roles to this auth ticket                        const int TimeoutInMinutes = 100; // TODO: look up the right value from the web.config file                        var ticket = new FormsAuthenticationTicket(                            2,                            response.ClaimedIdentifier,                            DateTime.Now,                            DateTime.Now.AddMinutes(TimeoutInMinutes),                            false, // non-persistent, since login is automatic and we wanted updated roles                            string.Join(";", roles.ToArray()));                        HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));                        Response.SetCookie(cookie);                        Response.Redirect(Request.QueryString["ReturnUrl"] ?? FormsAuthentication.DefaultUrl);                        break;                    default:                        break;                }            }            return RedirectToAction("Index", "Home");        }        public IFormsAuthenticationService FormsService { get; set; }        protected override void Initialize(RequestContext requestContext)        {            if (FormsService == null) { FormsService = new FormsAuthenticationService(); }            base.Initialize(requestContext);        }        public ActionResult LogOff()        {            FormsService.SignOut();            return RedirectToAction("Index", "Home");        }}

結合SsoOP,將個人的理解稍作解釋:

因為在web.config裡面使用下面的配置

    <authentication mode="Forms">      <forms name="OpenIdWebRingSsoRelyingParty" loginUrl="~/Account/LogOn"  protection="All"        path="/"        timeout="900" />    </authentication>    <authorization>      <deny users="?"/>    </authorization>

因為使用了Forms模式,在沒有登入的情況下,無論訪問任何資源,都會使請求轉到Account的LogOn Action中,在LogOn中,程式會先向OP的Identifier驗證是否存在ProviderEndpoint,OP通過OpenIdController的Xrds Action(既OP節中的Xrds.cshtml視圖內容)告訴RP這個提供者是存在併合法的,然後RP向提供者請求認證,反過來,OP倒也要確認RP是否存在併合法(使用RP中的Xrds.cshtml),如果沒有問題OP還要驗證請求認證的RP是否在白名單中,這個白名單中必須要和returnToBuilder.Path = "/Account/LogOn";這個值完全一致,比如這裡在LogOn後面沒有"/"號,那麼在白名單中,你就必須使用http://localhost:1220/Account/LogOn,而不能在後面加上“/”號,否則就會不通過。如果一切OK,沒有問題頁面將轉向OP的登入頁面,本例中為Account/LogOn,使用者輸入正確的使用者和密碼(本例User:bob;Password:test)。

登入完成後,根據LogOn中的代碼return RedirectToAction("Identifier", "OpenId");,請求會轉向OpenId/Identifier,程式會先去準備響應資料,這些資料中包含了登入使用者資訊,熟悉openid的人知道,openid總是使用一個url+使用者名稱代表使用者名稱,這個url其實就是另一個發現OP的地方,為什麼是另一個?還有一個在哪裡呢?就在OpenId/Identifier裡面呀,(因為還沒有對DotNetOpenAuth深入研究,因此,對於官方樣本中“服務發現”這個機制還有點模糊,個人感覺應該就是相當於驗證是否相任之類的吧,Identifier應該屬於登入前和登入階段的,當登入完成後使用使用者名稱中地址裡面的驗證了?),接下來使用ProviderEndpoint.SendResponse();向用戶端發送登入結果,並使用return_to裡面的資訊將請求轉到了RP的LogOn中,(在這個過程,RP將使用OP中AskUser“發現”服務提供者。)在LogOn中,根據IAuthenticationResponse的狀態資訊,確定是登入成功還是登入失敗(會攜帶失敗原因資訊)來確定請求轉向,既然咱都有範例程式碼了,應該就不會失敗吧,所以Home/Index會如期而至。

SsoRP樣本有兩個,一個是純MVC模式的,一個是使用MVC + WebForms模式的。

DotNetOpenAuth的資料現在貌似很少,個人對其的研究現在還處於Step by Step的階段,只能說跟著官方的樣本能做出一個MVC實現,但是對於很多具體的原理還是相當不熟悉,這樣本只能是解決有和沒有的問題,本文中的謬誤還望大家不吝賜教,希望有人能發更多有深度的資料。

樣本程式

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.