zoukankan      html  css  js  c++  java
  • 翻译:Single SignOn for Everyone (转自Anders Liu的.NET空间)

    前一阵写了一篇Blog,给出了一些SSO的资料(http://www.cnblogs.com/AndersLiu/archive/2007/05/25/760041.html)。现在把其中的一篇翻译出来。

    翻译:Single Sign-On for Everyone
    原文地址:http://bbs.hidotnet.com/22656/ShowPost.aspx

    单点登录(Single Sign-OnSSO)是这些天的热点话题。我的很多客户都有多个Web应用,运行在不同子域的不同.NET Framework版本中,甚至是不同的域中。他们都希望用户能够只登录一次,就能在各个不同的Web站点中保持登录状态。今天我们来一起看看如何在各种不同的场景中实现SSO。我们首先从最简单的情况开始,然后逐步构建它:

    1. 虚拟子目录中的父、子应用之间的SSO
    2. 使用不同授权凭证(用户名映射)的SSO
    3. 同一域下的两个子域中的Web应用之间的SSO
    4. 不同.NET版本下的应用之间的SSO
    5. 不同域之众的两个应用之间的SSO
    6. 混合模式验证(FormsWindows)中的SSO

    1. 虚拟子目录中的父、子应用之间的SSO

      假设有两个.NET应用——FooBar,并且Bar位于Foo的一个虚拟子目录中(http://foo.com/bar)。两个应用都实现了Forms验证。实现Forms验证需要重写Application_AuthenticateRequest,在这里进行验证,并在验证成功后调用FormsAuthentication.RedirectFromLoginPage,将登录的用户名(或系统中用于标识用户的其他信息)作为参数传递进去。在ASP.NET中,登录用户状态通过保存在客户端Cookie中进行持久化。当调用RedirectFromLoginPage时,就会创建一个Cookie,其中包含了加密的、带有登录用户名的FormsAuthenticationTicketWeb.Config中有一节用于定义如何创建该Cookie

    <authentication mode="Forms"> 

        
    <forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" /> 

    </authentication> 


    <authentication mode="Forms"> 

        
    <forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" /> 

    </authentication> 

      这里最重要的两个属性是nameprotection。如果在FooBar中,这两个属性是匹配的,那么它们就能在同样的保护级别上使用相同的Cookie,也就实现了SSO

    <authentication mode="Forms"> 

        
    <forms name=".SSOAuth" protection="All" timeout="60" loginUrl="login.aspx" /> 

    </authentication>

      当将protection属性设置为“All”以后,会同时对Cookie进行加密盒验证(通过散列值)。默认的验证和加密密钥存储在Machine.Config中,并且可以在应用程序的Web.Config中重写。其默认值为:

    <machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey=" AutoGenerate,IsolateApps" validation="SHA1" />

      IsolateApps意味着将为每个应用程序都生成一个不同的密钥。我们不能这样做。为了在所有应用程序中都能加密/解谜Cookie,需要移除IsolateApps属性,并为使用SSO的所有应用程序指定相同的具体密钥:

    <machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" />

      如果你正在针对不同的用户存储进行验证,这就是所有需要做的——对Web.Config的一点修改。

    2. 使用不同授权凭证(用户名映射)的SSO

      但是,如果Foo应用使用其自己的数据库,而Bar应用程序使用Membership API或其他形式的验证呢?在这种情况下,为Foo创建的Cookie并不适用于Bar,因为Bar并不理解其中包含的用户名。

      为了使其工作,需要创建第二个验证
    Cookie,专门用于Bar应用。还需要一种方式来将Foo用户映射到Bar用户。假设Foo应用中登录了一个“John Doe”用户,并且经过检测发现这个用户在Bar应用中的标识是“johnd”。在Foo的验证方法中需要添加下面的代码:

    FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1"johnd", DateTime.Now, DateTime.Now.AddYears(1), true""); 

    HttpCookie cookie 
    = new HttpCookie(".BarAuth"); 

    cookie.Value 
    = FormsAuthentication.Encrypt(fat); 

    cookie.Expires 
    = fat.Expiration; 

    HttpContext.Current.Response.Cookies.Add(cookie); 


    FormsAuthentication.RedirectFromLoginPage(
    "John Doe"); 

      硬编码的用户名仅仅用于演示目的。这段代码为Bar应用创建了FormsAuthenticationTicket,并用从Bar应用的上下文中找到的用户名对其进行了填充。然后调用了RedirectFromLoginPageFoo应用创建了正确的验证Cookie。如果你将两个应用程序的验证Cookie名字改成了相同的(见前面的示例),那么要注意现在他们是不同的了,我们无需再为每个站点使用相同的Cookie了:

    <authentication mode="Forms"> 

        
    <forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/> 

    </authentication> 


    <authentication mode="Forms"> 

        
    <forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/> 

    </authentication> 

      现在,只要用户登录到Foo,他就会被映射到Bar用户,并在会随着Foo验证票据创建一个Bar验证票据。如果希望相反的方向也能工作,只需在Bar应用中添加类似的代码即可:

    FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1"John Doe", DateTime.Now, DateTime.Now.AddYears(1), true""); 
    HttpCookie cookie 
    = new HttpCookie(".FooAuth"); 
    cookie.Value 
    = FormsAuthentication.Encrypt(fat); 
    cookie.Expires 
    = fat.Expiration; 
    HttpContext.Current.Response.Cookies.Add(cookie); 
    FormsAuthentication.RedirectFromLoginPage(
    "johnd"); 

      但仍然要确保Web.Config中的<machineKey>元素中为两个应用提供了匹配的验证和加密密钥。

    3. 同一域下的两个子域中的Web应用之间的SSO

      现在假设FooBar配置为在不同的域http://foo.comhttp://bar.foo.com中运行。前面的代码都不能使用了,因为Cookies将被存放到不同的文件中,并且应用程序彼此看不到(对方的Cookie)。为了使其能够工作,我们需要创建域级别的Cookies,并使其对所有子域可见。这样我们就不能使用RedirectFromLoginPage方法了,因为它不适合创建域级别的Cookie。我们可以手动完成这一工作:

    FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1"johnd", DateTime.Now, DateTime.Now.AddYears(1), true""); 

    HttpCookie cookie 
    = new HttpCookie(".BarAuth"); 

    cookie.Value 
    = FormsAuthentication.Encrypt(fat); 

    cookie.Expires 
    = fat.Expiration; 

    cookie.Domain 
    = ".foo.com";  // Highlight 

    HttpContext.Current.Response.Cookies.Add(cookie); 


    FormsAuthenticationTicket fat 
    = new FormsAuthenticationTicket(1"John Doe", DateTime.Now, DateTime.Now.AddYears(1), true""); 

    HttpCookie cookie 
    = new HttpCookie(".FooAuth"); 

    cookie.Value 
    = FormsAuthentication.Encrypt(fat); 

    cookie.Expires 
    = fat.Expiration; 

    cookie.Domain 
    = ".foo.com";  // Highlight 

    HttpContext.Current.Response.Cookies.Add(cookie); 


      注意高亮显示的行(Anders Liu:为了避免格式问题,我使用的是注释“// Highlight”)。通过明确地将Cookie的域设定为“.foo.com”,可以确保在http://foo.comhttp://bar.foo.com以及其他子域中都能看到该Cookie。你也可以将Bar的验证Cookie域设置为“bar.foo.com”。这样更加安全,因为其他子域看不到它。注意RFC 2109Cookie域值中要求两个periods,因此我们在前面添加了一个period——“.foo.com”。

      另外,确保在每个应用的
    Web.Config中使用相同的<machineKey>元素。只有一种特殊情况,接下来的小节将探讨这一情况。


    4.
    不同.NET版本下的应用之间的SSO


      有一种可能是
    FooBar应用运行在不同版本的.NET中。这是前面的例子就不能工作了。这是因为ASP.NET 2.0使用了不同的加密方法对验证票据进行加密。ASP.NET 1.1使用的是3DES,而ASP.NET 2.0使用的是AES。幸运的是,ASP.NET 2.0为了向后兼容,提供了一个新的属性:


    <machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" decryption="3DES" />

      设置decryption="3DES"可以让ASP.NET 2.0使用老的加密方法,这样Cookies就又匹配了。不要向ASP.NET 1.1Web.Config中添加这个属性,否则会导致错误。

    5. 不同域之众的两个应用之间的SSO

      至此为止我们成功地创建了共享的验证
    Cookie,但如果FooBar位于不同的域——http://foo.comhttp://bar.com——中呢?它们不可能共享Cookie,也不能彼此创建第二Cookie。这种情况下,每个站点需要创建自己的Cookies,并调用其他站点来验证用户是否已经在别处登录了。完成这一工作的一种方法就是通过一些页的重定向


      为了实现这一目的,我们分别在两个
    Web站点中都创建一个特殊的页面(我们称之为sso.aspx)。这个页面的目的就是检查其域中是否存在Cookie,并返回登录的用户名,这样其他应用可以在对应的域中创建类似的Cookie。下面是来自Bar.comsso.aspx


    <%@ Page Language="C#" %> 


    <script language="C#" runat="server"> 



    void Page_Load() 



        
    // this is our caller, we will need to redirect back to it eventually 

        UriBuilder uri 
    = new UriBuilder(Request.UrlReferrer); 


        HttpCookie c 
    = HttpContext.Current.Request.Cookies[".BarAuth"]; 


        
    if (c != null && c.HasKeys) // the cookie exists! 

        


            
    try 

            


                string cookie 
    = HttpContext.Current.Server.UrlDecode(c.Value); 

                FormsAuthenticationTicket fat 
    = FormsAuthentication.Decrypt(cookie);         


                uri.Query 
    = uri.Query + "&ssoauth=" + fat.Name; // add logged-in user name to the query 

            }
     

            
    catch 

            


            }
     

        }
     

        Response.Redirect(uri.ToString()); 
    // redirect back to the caller 

    }
     


    </script> 


      这个页面总是会重定向回调用方。如果Bar.com中存在验证Cookie,会解密用户名并通过查询字符串中的ssoauth参数返回。

      在另外一端(Foo.com),我们需要像http请求处理流水线中插入一些代码。可以在Application_BeginRequest事件中或者在一个自定义的HttpHandlerHttpModule中。其用意在于在所有的页面请求的尽可能早的地方检验验证Cookie是否存在:

    1)
    如果Foo.com中存在验证Cookie,继续处理请求。此时用户已登录Foo.com

    2) 如果验证Cookie不存在,重定向到Bar.com/sso.aspx
    3) 如果当前请求从Bar.com/sso.aspx重定向回来,分析ssoauth参数并在必要时创建验证Cookie

      这看起来相当简单,但要注意无限循环:


    // see if the user is logged in 

    HttpCookie c 
    = HttpContext.Current.Request.Cookies[".FooAuth"]; 


    if (c != null && c.HasKeys) // the cookie exists! 



        
    try 

        


            
    string cookie = HttpContext.Current.Server.UrlDecode(c.Value); 

            FormsAuthenticationTicket fat 
    = FormsAuthentication.Decrypt(cookie); 

            
    return// cookie decrypts successfully, continue processing the page 

        }
     

        
    catch 

        


        }
     

    }
     


    // the authentication cookie doesn't exist - ask Bar.com if the user is logged in there 

    UriBuilder uri 
    = new UriBuilder(Request.UrlReferrer); 


    if (uri.Host != "bar.com" || uri.Path != "/sso.aspx"// prevent infinite loop 



        Response.Redirect(http:
    //bar.com/sso.aspx); 

    }
     

    else 



        
    // we are here because the request we are processing is actually a response from bar.com 


        
    if (Request.QueryString["ssoauth"== null

        


            
    // Bar.com also didn't have the authentication cookie 

            
    return// continue normally, this user is not logged-in 

        }
     else 

        



            
    // user is logged in to Bar.com and we got his name! 

            
    string userName = (string)Request.QueryString["ssoauth"]; 


            
    // let's create a cookie with the same name 

            FormsAuthenticationTicket fat 
    = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddYears(1), true""); 

            HttpCookie cookie 
    = new HttpCookie(".FooAuth"); 

            cookie.Value 
    = FormsAuthentication.Encrypt(fat); 

            cookie.Expires 
    = fat.Expiration; 

            HttpContext.Current.Response.Cookies.Add(cookie); 

        }
     

    }
     



      两个站点都同样需要这段代码,但要在每个站点中使用正确的Cookie名字(.FooAuth vs. .BarAuth)。由于实际上并没有共享Cookie,所以应用程序可以具有不同的<machineKey>元素。无需同步加密和验证密钥。

      很多人可能比较担心在查询字符串中传递用户名所带来的安全隐患。很多方法可以对其进行保护。首先,要检查引用方,不接受来自任何源的
    ssoauth参数,但除了bar.com/sso.asp(或foo.com/sso.aspx)。其次,可以很容易地使用共享密钥对用户名进行加密。如果FooBar使用了不同的验证机制,也可以用类似的方式传递用户的附加信息(例如email地址)。


    6.
    混合模式验证(FormsWindows)中的SSO


      到现在为止,我们一直在处理
    Forms验证的情况。但如果我们希望对于Internet用户首先采用Forms验证,如果验证失败,再检查是否是NT域中的Intranet用户并进行验证。理论上,我们可以通过下面的参数来检查是否与请求关联了一个Windows已登录用户:

    Request.ServerVariables["LOGON_USER"


      然而,除非站点禁用了匿名访问,否则该值一直为空。我们可以在
    IIS控制面板中禁用匿名访问,并启用集成Windows验证。这样LOGON_USER值中将包含已登录的Intranet用户的NT域名。但是所有的Internet用户将面临Windows用户名和密码的挑战。这不爽。我们希望Internet用户可以通过Forms验证进行登录,而当失败的时候再检测其Windows域凭证。


      解决这一问题的一个方法是,为
    Intranet用户提供一个特殊的入口页,在这里启用集成Windows验证,验证域用户,然后创建一个Forms Cookie并导航到主站点。我们甚至可以通过Server.Transfer来隐藏Intranet用户访问了不同的页面这一事实。


      还有一种简单的解决方案。因为
    IIS处理验证过程,如果一个Web站点启用了匿名访问,IIS会将请求正确地传递给ASP.NET 运行时。它不会尝试执行任何类型的验证。然而,如果请求的结果是一个验证错误(401),IIS会尝试特定于该站点的另外一种验证方法。你可以同时启用匿名访问和集成Windows验证,然后再Forms验证失败后执行下面的代码:


    if (System.Web.HttpContext.Current.Request.ServerVariables["LOGON_USER"== ""

        System.Web.HttpContext.Current.Response.StatusCode 
    = 401

        System.Web.HttpContext.Current.Response.End(); 

    }
     

    else 



        
    // Request.ServerVariables["LOGON_USER"] has a valid domain user now! 

    }
     


      这段代码执行时,会首先检测域用户并得到一个空的字符串。然后它会终止当前请求并向IIS返回验证错误(401)。这将导致IIS使用另外一种验证机制,在这种情况下是集成Windows验证。如果用户已经登录到域,请求会被重复一次,此时会填充NT域用户信息。如果用户没有登录到域,他将有三次机会输入Windows用户名/密码。如果用户无法在三次尝试之内完成登录,他会得到403错误(拒绝访问)。

    小结


      我们讨论了在两个
    ASP.NET应用之间进行的各种场景的单点登录。当然也可以实现不同平台间的异构系统上的SSO。其思路是同样的,但实现起来可能需要一些创造性的想法。

  • 相关阅读:
    【转】win8.1下安装ubuntu
    Codeforces 1025G Company Acquisitions (概率期望)
    Codeforces 997D Cycles in Product (点分治、DP计数)
    Codeforces 997E Good Subsegments (线段树)
    Codeforces 1188E Problem from Red Panda (计数)
    Codeforces 1284E New Year and Castle Building (计算几何)
    Codeforces 1322D Reality Show (DP)
    AtCoder AGC043C Giant Graph (图论、SG函数、FWT)
    Codeforces 1305F Kuroni and the Punishment (随机化)
    AtCoder AGC022E Median Replace (字符串、自动机、贪心、计数)
  • 原文地址:https://www.cnblogs.com/silva/p/900360.html
Copyright © 2011-2022 走看看