如何在各种不同的应用场景中实现SSO?

来自Wenbanzhu
跳转至: 导航搜索

待解问题.png 问题

如何在各种不同的应用场景中实现SSO?.jpg
--Ly651686474 2010年4月22日 (四) 11:00 (CST)

问题具体描述:在各种不同的应用场景中如何实现sso?


最新回答.png 回答

方法

以下是在不同的应用场景中实现SSO的方法

1、虚拟目录的主应用和子应用之间实现SSO

  • 假设有两个.Net的Web应用程序-Foo和Bar,Bar运行在Foo虚拟目录的子目录(http://foo.com/bar)。二者都实现了Forms认证。
  • 实现Forms认证需要我们重写Application_AuthenticateRequest,在这个时机我们完成认证一旦通过验证就调用一下FormsAuthentication.RedirectFromLoginPage。这个方法接收的参数是用户名或者其它的一些身份信息。
  • 在Asp.net中登录用户的状态是持久化存储在客户端的cookie中,当你调用RedirectFromLoginPage时就会创建一个包含加密令牌FormsAuthenticationTicket的cookie,cookie名就是登录用户的用户名。下面的配置节在Web.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>

  • 比较重要的两个属性是 name 和protection。按照下面的配置就可以让Foo和Bar两个程序在同样的保护级别下读写Cookie,这就实现了SSO的效果:

<authentication mode="Forms">

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

</authentication>

  • 当 protection属性设置为“All”,通过Hash值进行加密和验证数据都存放在Cookie中。默认的验证和加密使用的Key都存储在machine.config文件,我们可以在应用程序的Web.Config文件覆盖这些值,默认值如下:

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

  (IsolateApps表示为每个应用程序生成不同的Key)。我们不能使用这个。为了能在多个应用程序中使用相同的Key来加密解密cookie,我们可以移除IsolateApps 选项或者更好的方法是在所有需要实现SSO的应用程序的Web.Config中设置一个具体的Key值:

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

  如果你使用同样的存储方式,实现SSO只是改动一下Web.config而已。

2、使用不同认证机制实现SSO(username mapping)

  • 要是FOO站点使用database来做认证,Bar站点使用Membership API或者其它方式做认证,这种情景中FOO站点创建的cookie对Bar站点毫无用处,因为cookie中的用户名对Bar没有什么意义。
  • 要想cookie起作用,就需要再为Bar站点创建一个认证所需的cookie。这里你需要为两个站点的用户做一下映射,假如有一个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站点的上下文中就是有意义的了。这时再调用 RedirectFromLoginPage创建正确的认证cookie,上面的例子统一了Forms 认证的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站点登录在Foo站点通行,那么代码就会是这样:

  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>配置节有相同的加密和解密的Key。

3、同一域名中,各子域名下应用程序间实现SSO

  • 若是Foo Bar两个站点运行在不同的域名下: http://foo.com 和http://bar.foo.com 。上面的代码又不起作用了,因为cookie会存储在不同的文件中,各自的cookie对其它网站不可见。
  • 为了能让它起作用我们需要创建域级cookie,因为域级cookie对子域名都是可见的,这里我们也不能再使用 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";

  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";

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

  • 注意cookie.Domain = ".foo.com";这一行明确指定了cookie的域名为".foo.com",这样我们就保证了cookie对 http://foo.comhttp://bar.foo.com 以及其它子域名都是可见的。可以通过设置Bar站点的认证cookie的域名为“bar.foo.com”。这样对于其它子域名的站点它的cookie也是不可见的。注意 RFC 2109 要求cookie前面有两个周期所以我们添加了一个过期时间。(cookie值实际上是一个字符串,各参数用逗号隔开)。
  • 再次提醒,这里还是需要统一一下各个站点的Web.config的<machineKey>配置节的Key值。

4、运行在不同版本.Net下应用程序间实现SSO

  • 要是Foo和Bar站点运行在不同的.Net环境中,上面的例子都行不通。这是由于Asp.net 2.0使用了不同于1.1的加密算法,1.1版本使用的是3DES,2.0是AES.万幸,Asp.net2.0中有一个属性可以兼容1.1:

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

  • 设置decryption="3DES"就会让 ASP.NET 2.0使用旧版本的加密算法使cookie能够正常使用,不要企图在Asp.net1.1的Web.config文件中添加这个属性,那会报错。

5、两个不同域名下的应用程序实现SSO

  • 我们已经成功的创建了可以共享的认证Cookie,但是如果Foo站点和Bar站点在不同域名下呢,例如: http://foo.comhttp://bar.com 。他们不能共享cookie也不能为对方在创建一个可读的cookie,这种情况下每个站点需要创有各自的cookie,调用其它站点的页面来验证用户是否登录。其中一种实现方式就是使用一系列的重定向。
  • 为了实现上述目标,需要在每个站点都创建一个特殊的页面(比如:sso.aspx)。这个页面的作用就是来检查该域名下的cookie是否存在并返回已经登录用户的用户名,这样其它站点也可以为这个用户创建一个cookie了,下面是Bar.com的sso.aspx:

Bar.com:

<%@ 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 Rquest处理的管道中添加一些的代码,可以是Web应用程序的 Application_BeginRequest 事件或者是自定义的HttpHandler或HttpModule。基本思想就是在所有Foo.com的页面请求之前做拦截,尽早的检查验证cookie是否存在:
    • 如果Foo.com的认证cookie已经存在,就继续处理请求,用户在Foo.com登录过;
    • 如果认证Cookie不存在就重定向到Bar.com/sso.aspx;
    • 如果现在的请求是从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并不是真正意义上的共享,因为Web应用程序的有不同的<machineKey>配置节,这里没有必要统一加密和解密的Key。
  • 有些人把在url里面把用户名当作参数传递视为畏途,实际上有两件事情可以做来保护:
    • 首先我们可以检查引用页参数不接受bar.com/sso.aspx(or foo.com/ssp.aspx)以外的站点;
    • 其次,用户名可以可以通过相同的Key做一下加密,如果Foo和Bar使用不同的认证机制,额外的用户信息(比如email地址)同样也可以传递过去。

6、混合身份验证模式下(Forms and Windows)实现SSO

  • 上面都是处理的Forms认证,要是这样设计认证过程呢:先做Forms认证,如果没有通过就检查Intranet用户是否已经在NT域上登录过了,这个思路我们需要检查下面的参数来看和请求关联的Windows logo信息:Request.ServerVariables["LOGON_USER"]
  • 但是除非我们的站点都是禁用匿名登录的,否则这个值总是空的,可以在IIS的控制面板禁用匿名登录,并为站点启用Windows集成认证。这样LOGON_USER 值就包含了NT域登录用户的名字,但是所有Internet用户的都会遇到用户名和密码的难题,这就不好了,我们要让Internet用户使用Forms认证要是这种方式失败了再使用Windows域认证。
    • 这个问题的解决方法之一就是为Intranet用户设置一个特殊的入口页面:Windows集成认证方式可用,验证域用户,创建Forms cookie重定向到主站点。甚至可以隐藏这样一个事实:由于Server.TransferIntranet用户实际上访问了不同的页面。
    • 也有一个简单的解决方法,基础是IIS掌控认证处理。如果站点对匿名用户可用。IIS就把请求传递给Asp.net运行,并试图进行认证要是失败了就引发一个401错误,IIS会试图寻找另外该站点的其它认证方式,你要设置匿名访问和集成认证可用并在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!

}

  • 这段代码执行时,会检查域用户并取得一个空的初始值,这回终止当前请求并返回认证的401错误到IIS,这就让IIS自动选择另外的认证机制,Windows集成认证方式就是候选方式。如果用户可以登录到域,请求就可以继续,并附加上了NT域用户的信息。如果用户没有在域中登录会有三次输入用户名密码的机会,如果三次失败,他就会得到一个403错误(AccessDenied)。

补充

要实现SSO,需要以下主要的功能

1、所有应用系统共享一个身份认证系统。统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。

2、所有应用系统能够识别和提取ticket信息。要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。

另外需要注意的是

1、单一的用户信息数据库并不是必须的。有许多系统不能将所有的用户信息都集中存储,应该允许用户信息放置在不同的存储中,如下图所示。事实上,只要统一认证系统,统一ticket的产生和效验,无论用户信息存储在什么地方,都能实现单点登录。

2、统一的认证系统并不是说只有单个的认证服务器

  • 认证服务器之间要通过标准的通讯协议,互相交换认证信息,就能完成更高级别的单点登录。
  • 如:当用户在访问应用系统1时,由第一个认证服务器进行认证后,得到由此服务器产生的ticket。当他访问应用系统2的时候,认证服务器2能够识别此ticket是由第一个服务器产生的,通过认证服务器之间标准的通讯协议(例如SAML)来交换认证信息,仍然能够完成SSO的功能。

参考网站

明星版主.png 版主: 本页回答由版主“Ly651686474”负责,您可以查看Ly651686474介绍和编辑或者给Ly651686474留言


分类浏览.png 分类网络|技术|sso|实现|应用|场景|不同|如何

讨论.png 讨论

关于“如何在各种不同的应用场景中实现SSO?”的留言:

目前暂无留言

新增相关留言