ASP.NET MVC 3 使用 DotNetOpenAuth 实现SSO

 

转载【http://www.cnblogs.com/think8848/archive/2011/03/06/1972195.html

听到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(真不明白为什么被墙了,想看的FQ吧,源码托管地址)这么个东西了,看介绍似乎就涵盖了我想要的一切功能!

文档可能是最让人又爱又恨的东西了,自已不愿意写文档,但都希望使用别人的东西时有完备的文档。

前两天在学习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:XRDS
	xmlns: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:XRDS
	xmlns: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实现,但是对于很多具体的原理还是相当不熟悉,这示例只能是解决有和没有的问题,本文中的谬误还望大家不吝赐教,希望有人能发更多有深度的资料。

示例程序

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。