使用WIF实现单点登录Part III —— 正式实战 -摘自网络

经过前两篇文章,估计大家对WIF已经有比较充分的认识了,估计大家在经过了枯燥的理论以后,肯定是摩拳擦掌赶紧付诸于行动了。微笑没办法,咱们程序员就是这个毛病。那好吧,我也不那么多废话了,直接进入正题吧。

我们接下来的demo将包括以下的工程:

 

  1. SiteA —— 基于.net framework 4.5的MVC 4程序,使用WIF 4.5的SDK,第一个RP
  2. SiteB —— 基于.net framework 4.5的MVC 4程序,使用WIF 3.5的SDK,第二个RP
  3. SiteC —— 基于.net framework 4.0的MVC 4程序,使用WIF 3.5的SDK,第三个RP
  4. SiteD —— 基于.net framework 4.0 的WebApplication程序,使用WIF 3.5的SDK,第四个RP
  5. STS —— 基于.net framework 4.5 的MVC 4程序,作为IP

 

 

一、创建第一个RP

以管理员身份打开vs2012,在起始页上点击“新建项目”,在左边的“模板”树下,展开“其它项目类型”,然后选择“Visual Studio解决方案”,“名称”输入框里输入WIFSSO,然后选择解决方案的路径后点击”确定“,如图:

在”解决方案资源管理器“中,在新建好的解决方案上点右键,选择”添加“->”新建项目“。在弹出的对话框中选择”ASP.NET MVC 4 Web应用程序“,记得.Net Framework版本选4.5,名称起名为”SiteA“,然后点确定,如图:

在弹出的“新ASP.NET MVC 4项目”对话框中直接点“确定”,第一个RP项目新建完成后,添加以下两个引用:System.IdentityModel和System.IdentityModel.Services。这次的教程不使用Identity and Access Tool,而是直接修改web.config文件,这样能使大家对WIF的配置有更深入的了解。

打开web.config文件,将configSections节里的entityFramework配置节点删掉,因为我们不需要用到Entity Framework。最好把web.config中关于Entity Framework相关的配置全都删掉,因为我们都用不上。然后加上以下这两个节点:

 

  1. <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />  
  2.    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />  
 <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />

将authentication节的mode属性设为None,并把里面的form节点删掉,因为我们采用的是WIF的身份验证方式,而不是传统的Forms身份验证。然后增加authorization节点,不允许匿名用户访问站点:

 

 

  1. <authorization>  
  2.   <deny users="?"/>  
  3. </authorization>  
    <authorization>
      <deny users="?"/>
    </authorization>

在system.webServer节点下增加2个HttpModule的配置节点:

 

 

  1. <modules>  
  2.   <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />  
  3.   <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />  
  4. </modules>  
    <modules>
      <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
      <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
    </modules>

最后,增加WIF的配置节点:

 

 

  1. <system.identityModel>  
  2.   <identityConfiguration>  
  3.     <audienceUris mode="Always">  
  4.       <add value="http://www.sitea.com" />  
  5.     </audienceUris>  
  6.     <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">  
  7.       <trustedIssuers>  
  8.         <add name="http://www.sts.com" thumbprint="FD1425A2F30937786F46E52E43B01AFD54E5D64D"/>  
  9.       </trustedIssuers>  
  10.     </issuerNameRegistry>  
  11.   </identityConfiguration>  
  12. </system.identityModel>  
  13. <system.identityModel.services>  
  14.   <federationConfiguration>  
  15.     <cookieHandler requireSsl="false" />  
  16.     <wsFederation passiveRedirectEnabled="true" issuer="http://www.sts.com" realm="http://www.sitea.com" reply="http://www.sitea.com" requireHttps="false"/>  
  17.   </federationConfiguration>  
  18. </system.identityModel.services>  
  <system.identityModel>
    <identityConfiguration>
      <audienceUris mode="Always">
        <add value="http://www.sitea.com" />
      </audienceUris>
      <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <trustedIssuers>
          <add name="http://www.sts.com" thumbprint="FD1425A2F30937786F46E52E43B01AFD54E5D64D"/>
        </trustedIssuers>
      </issuerNameRegistry>
    </identityConfiguration>
  </system.identityModel>
  <system.identityModel.services>
    <federationConfiguration>
      <cookieHandler requireSsl="false" />
      <wsFederation passiveRedirectEnabled="true" issuer="http://www.sts.com" realm="http://www.sitea.com" reply="http://www.sitea.com" requireHttps="false"/>
    </federationConfiguration>
  </system.identityModel.services>

我来详细解释一下这些节点的意义。audienceUris指定了一组可以被RP接受的身份标识URI,只有这些配置中的URI范围内的令牌才可以被接受。这里,我把siteA配置在这里。trustedIssuers就是受信任的发行者,由于我们这个demo没有用到SSL,所以这里我指定的thumbprint是IIS Express的指纹,这个指纹在哪里可以获得呢?打开IIS管理器,在左侧树点击根节点,然后在“功能视图”里双击“服务器证书",如下图:

在打开的证书列表里,找到IIS Express Development Certificate,双击,在弹出的”证书“对话框中点击“详细信息”页签,找到“指纹”然后点击,把框里的指纹拷下来,全都改成大写后粘贴到thumbnail的值里去:

接下来配置federationConfiguration节点,它表示配置WSFederationAuthenticationModule (WSFAM)  和SessionAuthenticationModule (SAM)  时使用联合身份验证通过的 WS 联合身份验证协议。这里我们使用WS 联合身份验证的身份验证模块 (WSFAM),关于该节点的详细配置信息,请参考:http://msdn.microsoft.com/zh-cn/library/office/apps/hh568665.aspx

好,这样一来,SiteA的配置就已经完成了,然后我们来加点代码。

打开/Views/Home/Index.cshtml,将原有的代码删掉,改为如下代码:

 

  1. @using System.Security.Claims  
  2. @{  
  3.     ViewBag.Title = "SiteA主页";  
  4.     ClaimsIdentity ci = User.Identity as ClaimsIdentity;  
  5.     if(ci!=null)  
  6.     {  
  7.         <h2>@ci.FindFirst(ClaimTypes.Name).Value</h2>  
  8.         <h2>@ci.FindFirst(ClaimTypes.Email).Value</h2>  
  9.     }  
  10. }  
  11.   
  12. <a href="http://www.sts.com/Account/LogOff">退出</a>  
@using System.Security.Claims
@{
    ViewBag.Title = "SiteA主页";
    ClaimsIdentity ci = User.Identity as ClaimsIdentity;
    if(ci!=null)
    {
        <h2>@ci.FindFirst(ClaimTypes.Name).Value</h2>
        <h2>@ci.FindFirst(ClaimTypes.Email).Value</h2>
    }
}

<a href="http://www.sts.com/Account/LogOff">退出</a>

代码很简单,只要当前用户处于已登录状态,就把用户的名称和Email显示在页面上。

 

至此,SiteA就已经完成了。你是不是迫不及待的想要运行了呢?别急,虽然有SiteA了,但还没有STS呢,现在启动SiteA,由于没登录,所以它会跳转到STS,但STS还不存在,所以会出错的。

 

二、创建STS

接下来我们来创建STS,在解决方案上新建项目,新建一个名为STS的MVC 4应用程序,.Net Framework选择4.5,项目模板选择“Internet应用程序",确定。

添加System.IdentityModel和System.IdentityModel.Services这两个引用,打开web.config,为forms节点添加两个属性:

 

  1. <forms loginUrl="~/Account/Login" timeout="2880" slidingExpiration="true" name=".STSASPAUTH" />  
<forms loginUrl="~/Account/Login" timeout="2880" slidingExpiration="true" name=".STSASPAUTH" />

在AppSettings里增加如下三个节点:

 

 

  1. <add key="IssuerName" value="PassiveSigninSTS" />  
  2. <add key="SigningCertificateName" value="CN=localhost" />  
  3. <add key="EncryptingCertificateName" value="" />  
    <add key="IssuerName" value="PassiveSigninSTS" />
    <add key="SigningCertificateName" value="CN=localhost" />
    <add key="EncryptingCertificateName" value="" />

同样禁止匿名用户访问:

 

 

  1. <authorization>  
  2.   <deny users="?"/>  
  3. </authorization>  
    <authorization>
      <deny users="?"/>
    </authorization>

在应用程序下新建一个名为Services的文件夹,在里面新建一个类文件,名为:CertificateUtil,用于获取证书,具体代码如下:

 

 

  1. public class CertificateUtil  
  2. {  
  3.     public static X509Certificate2 GetCertificate(StoreName name, StoreLocation location, string subjectName)  
  4.     {  
  5.         X509Store store = new X509Store(name, location);  
  6.         X509Certificate2Collection certificates = null;  
  7.         store.Open(OpenFlags.ReadOnly);  
  8.   
  9.         try  
  10.         {  
  11.             X509Certificate2 result = null;  
  12.             certificates = store.Certificates;  
  13.   
  14.             for (int i = 0; i < certificates.Count; i++)  
  15.             {  
  16.                 X509Certificate2 cert = certificates[i];  
  17.                 if (cert.SubjectName.Name.ToLower() == subjectName.ToLower())  
  18.                 {  
  19.                     if (result != null)  
  20.                         throw new ApplicationException(string.Format("subject Name {0}存在多个证书", subjectName));  
  21.                     result = new X509Certificate2(cert);  
  22.                 }  
  23.             }  
  24.   
  25.             if (result == null)  
  26.             {  
  27.                 throw new ApplicationException(string.Format("没有找到用于 subject Name {0} 的证书", subjectName));  
  28.             }  
  29.   
  30.             return result;  
  31.         }  
  32.         finally  
  33.         {  
  34.             if (certificates != null)  
  35.             {  
  36.                 for (int i = 0; i < certificates.Count; i++)  
  37.                 {  
  38.                     certificates[i].Reset();  
  39.                 }  
  40.             }  
  41.             store.Close();  
  42.         }  
  43.     }  
  44.   
  45. }  
    public class CertificateUtil
    {
        public static X509Certificate2 GetCertificate(StoreName name, StoreLocation location, string subjectName)
        {
            X509Store store = new X509Store(name, location);
            X509Certificate2Collection certificates = null;
            store.Open(OpenFlags.ReadOnly);

            try
            {
                X509Certificate2 result = null;
                certificates = store.Certificates;

                for (int i = 0; i < certificates.Count; i++)
                {
                    X509Certificate2 cert = certificates[i];
                    if (cert.SubjectName.Name.ToLower() == subjectName.ToLower())
                    {
                        if (result != null)
                            throw new ApplicationException(string.Format("subject Name {0}存在多个证书", subjectName));
                        result = new X509Certificate2(cert);
                    }
                }

                if (result == null)
                {
                    throw new ApplicationException(string.Format("没有找到用于 subject Name {0} 的证书", subjectName));
                }

                return result;
            }
            finally
            {
                if (certificates != null)
                {
                    for (int i = 0; i < certificates.Count; i++)
                    {
                        certificates[i].Reset();
                    }
                }
                store.Close();
            }
        }

    }

 

创建新类,名为Common,存放几个常量:

 

  1. public class Common  
  2. {  
  3.     public const string IssuerName = "IssuerName";  
  4.     public const string SigningCertificateName = "SigningCertificateName";  
  5.     public const string EncryptingCertificateName = "EncryptingCertificateName";  
  6.   
  7. }  
    public class Common
    {
        public const string IssuerName = "IssuerName";
        public const string SigningCertificateName = "SigningCertificateName";
        public const string EncryptingCertificateName = "EncryptingCertificateName";

    }

创建新类,名为SingleSignOnManager,用于注册RP以及获取RP列表:

 

 

  1. public class SingleSignOnManager  
  2. {  
  3.     const string SITECOOKIENAME = "StsSiteCookie";  
  4.     const string SITENAME = "StsSite";  
  5.   
  6.     /// <summary>  
  7.     /// Returns a list of sites the user is logged in via the STS  
  8.     /// </summary>  
  9.     /// <returns></returns>  
  10.     public static string[] SignOut()  
  11.     {  
  12.         if (HttpContext.Current != null &&  
  13.              HttpContext.Current.Request != null &&  
  14.              HttpContext.Current.Request.Cookies != null  
  15.             )  
  16.         {  
  17.             HttpCookie siteCookie =  
  18.                 HttpContext.Current.Request.Cookies[SITECOOKIENAME];  
  19.   
  20.             if (siteCookie != null)  
  21.                 return siteCookie.Values.GetValues(SITENAME);  
  22.         }  
  23.   
  24.         return new string[0];  
  25.     }  
  26.   
  27.     public static void RegisterRP(string SiteUrl)  
  28.     {  
  29.         if (HttpContext.Current != null &&  
  30.              HttpContext.Current.Request != null &&  
  31.              HttpContext.Current.Request.Cookies != null  
  32.             )  
  33.         {  
  34.             // get an existing cookie or create a new one  
  35.             HttpCookie siteCookie =  
  36.                 HttpContext.Current.Request.Cookies[SITECOOKIENAME];  
  37.             if (siteCookie == null)  
  38.                 siteCookie = new HttpCookie(SITECOOKIENAME);  
  39.   
  40.             siteCookie.Values.Add(SITENAME, SiteUrl);  
  41.   
  42.             HttpContext.Current.Response.AppendCookie(siteCookie);  
  43.         }  
  44.     }  
  45.   
  46. }  
    public class SingleSignOnManager
    {
        const string SITECOOKIENAME = "StsSiteCookie";
        const string SITENAME = "StsSite";

        /// <summary>
        /// Returns a list of sites the user is logged in via the STS
        /// </summary>
        /// <returns></returns>
        public static string[] SignOut()
        {
            if (HttpContext.Current != null &&
                 HttpContext.Current.Request != null &&
                 HttpContext.Current.Request.Cookies != null
                )
            {
                HttpCookie siteCookie =
                    HttpContext.Current.Request.Cookies[SITECOOKIENAME];

                if (siteCookie != null)
                    return siteCookie.Values.GetValues(SITENAME);
            }

            return new string[0];
        }

        public static void RegisterRP(string SiteUrl)
        {
            if (HttpContext.Current != null &&
                 HttpContext.Current.Request != null &&
                 HttpContext.Current.Request.Cookies != null
                )
            {
                // get an existing cookie or create a new one
                HttpCookie siteCookie =
                    HttpContext.Current.Request.Cookies[SITECOOKIENAME];
                if (siteCookie == null)
                    siteCookie = new HttpCookie(SITECOOKIENAME);

                siteCookie.Values.Add(SITENAME, SiteUrl);

                HttpContext.Current.Response.AppendCookie(siteCookie);
            }
        }

    }

创建新类,CustomSecurityTokenService,自定义令牌服务,继承SecurityTokenService,用于返回需要的声明令牌:

 

 

  1. public class CustomSecurityTokenService : SecurityTokenService  
  2. {  
  3.     private readonly SigningCredentials signingCreds;  
  4.     private readonly EncryptingCredentials encryptingCreds;  
  5.   
  6.     public CustomSecurityTokenService(SecurityTokenServiceConfiguration config)  
  7.         : base(config)  
  8.     {  
  9.         this.signingCreds = new X509SigningCredentials(  
  10.             CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.SigningCertificateName]));  
  11.   
  12.         if (!string.IsNullOrWhiteSpace(WebConfigurationManager.AppSettings[Common.EncryptingCertificateName]))  
  13.         {  
  14.             this.encryptingCreds = new X509EncryptingCredentials(  
  15.                 CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.EncryptingCertificateName]));  
  16.         }  
  17.     }  
  18.   
  19.     /// <summary>  
  20.     /// 此方法返回要发布的令牌内容。内容由一组ClaimsIdentity实例来表示,每一个实例对应了一个要发布的令牌。当前Windows Identity Foundation只支持单个令牌发布,因此返回的集合必须总是只包含单个实例。  
  21.     /// </summary>  
  22.     /// <param name="principal">调用方的principal</param>  
  23.     /// <param name="request">进入的 RST,我们这里不用它</param>  
  24.     /// <param name="scope">由之前通过GetScope方法返回的范围</param>  
  25.     /// <returns></returns>  
  26.     protected override ClaimsIdentity GetOutputClaimsIdentity(ClaimsPrincipal principal, RequestSecurityToken request, Scope scope)  
  27.     {  
  28.         //返回一个默认声明集,里面了包含自己想要的声明  
  29.         //这里你可以通过ClaimsPrincipal来验证用户,并通过它来返回正确的声明。  
  30.         string identityName = principal.Identity.Name;  
  31.         string[] temp = identityName.Split('|');  
  32.         ClaimsIdentity outgoingIdentity = new ClaimsIdentity();  
  33.         outgoingIdentity.AddClaim(new Claim(ClaimTypes.Email, temp[0]));  
  34.         outgoingIdentity.AddClaim(new Claim(ClaimTypes.DateOfBirth, temp[1]));  
  35.         outgoingIdentity.AddClaim(new Claim(ClaimTypes.Name, temp[2]));  
  36.         SingleSignOnManager.RegisterRP(scope.AppliesToAddress);  
  37.         return outgoingIdentity;  
  38.     }  
  39.   
  40.     /// <summary>  
  41.     /// 此方法返回用于令牌发布请求的配置。配置由Scope类表示。在这里,我们只发布令牌到一个由encryptingCreds字段表示的RP标识        /// </summary>  
  42.     /// <param name="principal"></param>  
  43.     /// <param name="request"></param>  
  44.     /// <returns></returns>  
  45.     protected override Scope GetScope(ClaimsPrincipal principal, RequestSecurityToken request)  
  46.     {  
  47.         // 使用request的AppliesTo属性和RP标识来创建Scope  
  48.         Scope scope = new Scope(request.AppliesTo.Uri.AbsoluteUri, this.signingCreds);  
  49.   
  50.         if (Uri.IsWellFormedUriString(request.ReplyTo, UriKind.Absolute))  
  51.         {  
  52.             if (request.AppliesTo.Uri.Host != new Uri(request.ReplyTo).Host)  
  53.                 scope.ReplyToAddress = request.AppliesTo.Uri.AbsoluteUri;  
  54.             else  
  55.                 scope.ReplyToAddress = request.ReplyTo;  
  56.         }  
  57.         else  
  58.         {  
  59.             Uri resultUri = null;  
  60.             if (Uri.TryCreate(request.AppliesTo.Uri, request.ReplyTo, out resultUri))  
  61.                 scope.ReplyToAddress = resultUri.AbsoluteUri;  
  62.             else  
  63.                 scope.ReplyToAddress = request.AppliesTo.Uri.ToString();  
  64.         }  
  65.         if (this.encryptingCreds != null)  
  66.         {  
  67.             // 如果STS对应多个RP,要选择证书指定到请求令牌的RP,然后再用 encryptingCreds   
  68.             scope.EncryptingCredentials = this.encryptingCreds;  
  69.         }  
  70.         else  
  71.             scope.TokenEncryptionRequired = false;  
  72.         return scope;  
  73.     }  
  74. }  
    public class CustomSecurityTokenService : SecurityTokenService
    {
        private readonly SigningCredentials signingCreds;
        private readonly EncryptingCredentials encryptingCreds;

        public CustomSecurityTokenService(SecurityTokenServiceConfiguration config)
            : base(config)
        {
            this.signingCreds = new X509SigningCredentials(
                CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.SigningCertificateName]));

            if (!string.IsNullOrWhiteSpace(WebConfigurationManager.AppSettings[Common.EncryptingCertificateName]))
            {
                this.encryptingCreds = new X509EncryptingCredentials(
                    CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.EncryptingCertificateName]));
            }
        }

        /// <summary>
        /// 此方法返回要发布的令牌内容。内容由一组ClaimsIdentity实例来表示,每一个实例对应了一个要发布的令牌。当前Windows Identity Foundation只支持单个令牌发布,因此返回的集合必须总是只包含单个实例。
        /// </summary>
        /// <param name="principal">调用方的principal</param>
        /// <param name="request">进入的 RST,我们这里不用它</param>
        /// <param name="scope">由之前通过GetScope方法返回的范围</param>
        /// <returns></returns>
        protected override ClaimsIdentity GetOutputClaimsIdentity(ClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
        {
            //返回一个默认声明集,里面了包含自己想要的声明
            //这里你可以通过ClaimsPrincipal来验证用户,并通过它来返回正确的声明。
            string identityName = principal.Identity.Name;
            string[] temp = identityName.Split('|');
            ClaimsIdentity outgoingIdentity = new ClaimsIdentity();
            outgoingIdentity.AddClaim(new Claim(ClaimTypes.Email, temp[0]));
            outgoingIdentity.AddClaim(new Claim(ClaimTypes.DateOfBirth, temp[1]));
            outgoingIdentity.AddClaim(new Claim(ClaimTypes.Name, temp[2]));
            SingleSignOnManager.RegisterRP(scope.AppliesToAddress);
            return outgoingIdentity;
        }

        /// <summary>
        /// 此方法返回用于令牌发布请求的配置。配置由Scope类表示。在这里,我们只发布令牌到一个由encryptingCreds字段表示的RP标识        /// </summary>
        /// <param name="principal"></param>
        /// <param name="request"></param>
        /// <returns></returns>
        protected override Scope GetScope(ClaimsPrincipal principal, RequestSecurityToken request)
        {
            // 使用request的AppliesTo属性和RP标识来创建Scope
            Scope scope = new Scope(request.AppliesTo.Uri.AbsoluteUri, this.signingCreds);

            if (Uri.IsWellFormedUriString(request.ReplyTo, UriKind.Absolute))
            {
                if (request.AppliesTo.Uri.Host != new Uri(request.ReplyTo).Host)
                    scope.ReplyToAddress = request.AppliesTo.Uri.AbsoluteUri;
                else
                    scope.ReplyToAddress = request.ReplyTo;
            }
            else
            {
                Uri resultUri = null;
                if (Uri.TryCreate(request.AppliesTo.Uri, request.ReplyTo, out resultUri))
                    scope.ReplyToAddress = resultUri.AbsoluteUri;
                else
                    scope.ReplyToAddress = request.AppliesTo.Uri.ToString();
            }
            if (this.encryptingCreds != null)
            {
                // 如果STS对应多个RP,要选择证书指定到请求令牌的RP,然后再用 encryptingCreds 
                scope.EncryptingCredentials = this.encryptingCreds;
            }
            else
                scope.TokenEncryptionRequired = false;
            return scope;
        }
    }

最后添加新类CustomSecurityTokenServiceConfiguration,继承SecurityTokenServiceConfiguration:

 

 

  1. public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration  
  2. {  
  3.     private static readonly object syncRoot = new object();  
  4.     private const string CustomSecurityTokenServiceConfigurationKey = "CustomSecurityTokenServiceConfigurationKey";  
  5.   
  6.     public CustomSecurityTokenServiceConfiguration()  
  7.         : base(WebConfigurationManager.AppSettings[Common.IssuerName])  
  8.     {  
  9.         this.SecurityTokenService = typeof(CustomSecurityTokenService);  
  10.     }  
  11.   
  12.     public static CustomSecurityTokenServiceConfiguration Current  
  13.     {  
  14.         get  
  15.         {  
  16.             HttpApplicationState app = HttpContext.Current.Application;  
  17.             CustomSecurityTokenServiceConfiguration config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration;  
  18.             if (config != null)  
  19.                 return config;  
  20.             lock (syncRoot)  
  21.             {  
  22.                 config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration;  
  23.                 if (config == null)  
  24.                 {  
  25.                     config = new CustomSecurityTokenServiceConfiguration();  
  26.                     app.Add(CustomSecurityTokenServiceConfigurationKey, config);  
  27.                 }  
  28.   
  29.                 return config;  
  30.             }  
  31.         }  
  32.     }  
  33. }  
    public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration
    {
        private static readonly object syncRoot = new object();
        private const string CustomSecurityTokenServiceConfigurationKey = "CustomSecurityTokenServiceConfigurationKey";

        public CustomSecurityTokenServiceConfiguration()
            : base(WebConfigurationManager.AppSettings[Common.IssuerName])
        {
            this.SecurityTokenService = typeof(CustomSecurityTokenService);
        }

        public static CustomSecurityTokenServiceConfiguration Current
        {
            get
            {
                HttpApplicationState app = HttpContext.Current.Application;
                CustomSecurityTokenServiceConfiguration config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration;
                if (config != null)
                    return config;
                lock (syncRoot)
                {
                    config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration;
                    if (config == null)
                    {
                        config = new CustomSecurityTokenServiceConfiguration();
                        app.Add(CustomSecurityTokenServiceConfigurationKey, config);
                    }

                    return config;
                }
            }
        }
    }

打开/Controllers/HomeController.cs,将Index()方法修改如下:

 

 

  1. public ActionResult Index()  
  2. {  
  3.     FederatedPassiveSecurityTokenServiceOperations.ProcessRequest(  
  4.         System.Web.HttpContext.Current.Request,  
  5.         User as ClaimsPrincipal,  
  6.         CustomSecurityTokenServiceConfiguration.Current.CreateSecurityTokenService(),  
  7.         System.Web.HttpContext.Current.Response);  
  8.     return View();  
  9. }  
        public ActionResult Index()
        {
            FederatedPassiveSecurityTokenServiceOperations.ProcessRequest(
                System.Web.HttpContext.Current.Request,
                User as ClaimsPrincipal,
                CustomSecurityTokenServiceConfiguration.Current.CreateSecurityTokenService(),
                System.Web.HttpContext.Current.Response);
            return View();
        }

打开/Controllers/AccountController.cs,将Login(LoginModel model, string returnUrl)方法修改如下:

 

 

  1.         [HttpPost]  
  2.         [AllowAnonymous]  
  3.         [ValidateAntiForgeryToken]  
  4.         public ActionResult Login(LoginModel model, string returnUrl)  
  5.         {  
  6.             var query = HttpUtility.ParseQueryString(Request.UrlReferrer.Query);  
  7.             if (model.UserName == "ojlovecd@csdn.net" && model.Password == "123456")  
  8.             {  
  9.                 FormsAuthentication.SetAuthCookie("ojlovecd@csdn.net|1983-10-22|oujian"false);  
  10.                 if (!string.IsNullOrEmpty(returnUrl))  
  11.                     return Redirect(returnUrl);  
  12.                 return RedirectToAction("Index""Home");  
  13.             }  
  14.             return View(model);  
  15.         }  
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl)
        {
            var query = HttpUtility.ParseQueryString(Request.UrlReferrer.Query);
            if (model.UserName == "ojlovecd@csdn.net" && model.Password == "123456")
            {
                FormsAuthentication.SetAuthCookie("ojlovecd@csdn.net|1983-10-22|oujian", false);
                if (!string.IsNullOrEmpty(returnUrl))
                    return Redirect(returnUrl);
                return RedirectToAction("Index", "Home");
            }
            return View(model);
        }

LogOff方法修改如下:

 

 

  1.         public ActionResult LogOff()  
  2.         {  
  3.             FormsAuthentication.SignOut();  
  4.             ViewData["AddressesExpected"] = SingleSignOnManager.SignOut().Distinct().ToArray();  
  5.             return View("Login");  
  6.         }  
        public ActionResult LogOff()
        {
            FormsAuthentication.SignOut();
            ViewData["AddressesExpected"] = SingleSignOnManager.SignOut().Distinct().ToArray();
            return View("Login");
        }

打开/Views/Account/Login.cshtml,添加以下代码:

 

 

  1. @{  
  2.     ViewBag.Title = "登录";  
  3.     var addressesExpected = ViewData["AddressesExpected"] as string[];  
  4.     if (addressesExpected != null)  
  5.     {  
  6.         foreach (var address in addressesExpected)  
  7.         {  
  8.     <img src="@(address)?wa=wsignoutcleanup1.0" style="display:none;" />  
  9.         }  
  10.     }  
  11.   
  12. }  
@{
    ViewBag.Title = "登录";
    var addressesExpected = ViewData["AddressesExpected"] as string[];
    if (addressesExpected != null)
    {
        foreach (var address in addressesExpected)
        {
    <img src="@(address)?wa=wsignoutcleanup1.0" style="display:none;" />
        }
    }

}

OK,至此STS也已经完成了。把SiteA和STS都部署到IIS上,然后打开C:\Windows\System32\Drivers\etc\hosts文件,添加几个站点:

 

  1. 127.0.0.1   www.sitea.com  
  2. 127.0.0.1   www.siteb.com  
  3. 127.0.0.1   www.sitec.com  
  4. 127.0.0.1   www.sited.com  
  5. 127.0.0.1   www.sts.com  
127.0.0.1	www.sitea.com
127.0.0.1	www.siteb.com
127.0.0.1	www.sitec.com
127.0.0.1	www.sited.com
127.0.0.1	www.sts.com

 

 

好了,在浏览器输入www.sitea.com,看看如何,它马上跳转到了www.sts.com的登录页面,输入ojlovecd@csdn.net,密码123456,确定,登录成功,跳回到了www.sitea.com,并显示出了用户名和Email:

点击退出,将注销当前用户,并跳转到登录页。

 

三、创建其它RP

OK,站点A搞定了,那其它站点如何呢?现在只是最简单的登录退出功能而已,说好的单点登录呢? 别急,接下来就一一实现。 新建基于.NET Framework4.5的MVC4程序,添加Microsoft.IdentityModel引用。修改web.config,configSections里添加如下节点:

  1. <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />  
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

Compilation里增加Microsoft.IdentityModel的程序集:

  1. <compilation debug="true" targetFramework="4.5" >  
  2.   <assemblies>  
  3.     <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>  
  4.   </assemblies>  
  5. </compilation>  
    <compilation debug="true" targetFramework="4.5" >
      <assemblies>
        <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </assemblies>
    </compilation>

身份验证改为None,添加authorization节点,禁止匿名用户访问:

  1. <authentication mode="None">  
  2. </authentication>  
  3. <authorization>  
  4.   <deny users="?" />  
  5. </authorization>  
    <authentication mode="None">
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>

添加三个httpModules:     

  1. <httpModules>  
  2.       <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />  
  3.       <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />  
  4.       <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />  
  5.     </httpModules>  
  6. system.webServer里添加以下三个modules:  
  7.     <modules >  
  8.       <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />  
  9.       <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />  
  10.       <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />  
  11.     </modules>  
<httpModules>
      <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </httpModules>
system.webServer里添加以下三个modules:
    <modules >
      <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
      <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
      <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
    </modules>

最后增加microsoft.identityModel节点:  

  1. <microsoft.identityModel>  
  2.    <service>  
  3.      <audienceUris mode="Always">  
  4.        <add value="http://www.siteb.com" />  
  5.      </audienceUris>  
  6.      <federatedAuthentication>  
  7.        <wsFederation passiveRedirectEnabled="true" issuer="http://www.sts.com" realm="http://www.siteb.com" reply="http://www.siteb.com"  requireHttps="false" />  
  8.        <cookieHandler requireSsl="false" />  
  9.      </federatedAuthentication>  
  10.      <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">  
  11.        <trustedIssuers>  
  12.          <add thumbprint="FD1425A2F30937786F46E52E43B01AFD54E5D64D" name="http://www.sts.com" />  
  13.        </trustedIssuers>  
  14.      </issuerNameRegistry>  
  15.    </service>  
  16.  </microsoft.identityModel>  
 <microsoft.identityModel>
    <service>
      <audienceUris mode="Always">
        <add value="http://www.siteb.com" />
      </audienceUris>
      <federatedAuthentication>
        <wsFederation passiveRedirectEnabled="true" issuer="http://www.sts.com" realm="http://www.siteb.com" reply="http://www.siteb.com"  requireHttps="false" />
        <cookieHandler requireSsl="false" />
      </federatedAuthentication>
      <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedIssuers>
          <add thumbprint="FD1425A2F30937786F46E52E43B01AFD54E5D64D" name="http://www.sts.com" />
        </trustedIssuers>
      </issuerNameRegistry>
    </service>
  </microsoft.identityModel>

以上配置跟SIteA差不多,只是WIF3.5和4.5的区别而已,在这里就不赘述了,要获取详细信息,请参考微软官方网站。 打开/Views/Home/Index.cshtml,将代码修改如下,在SiteB里我们显示Email和生日:

  1. @using Microsoft.IdentityModel.Claims  
  2. @{  
  3.     ViewBag.Title = "SiteB主页";  
  4.     ClaimsIdentity ci = User.Identity as ClaimsIdentity;  
  5.     if(ci!=null)  
  6.     {  
  7.         <h2>@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.Email).Value</h2>  
  8.         <h2>@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.DateOfBirth).Value</h2>  
  9.     }  
  10. }  
  11.   
  12.   
  13. <a href="http://www.sts.com/Account/LogOff">退出</a>  
@using Microsoft.IdentityModel.Claims
@{
    ViewBag.Title = "SiteB主页";
    ClaimsIdentity ci = User.Identity as ClaimsIdentity;
    if(ci!=null)
    {
        <h2>@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.Email).Value</h2>
        <h2>@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.DateOfBirth).Value</h2>
    }
}


<a href="http://www.sts.com/Account/LogOff">退出</a>

OK,部署到IIS上,然后运行,页面跳转到了sts的登录页面,输入用户名和密码,跳转,哎哟我去,怎么报错了:
原因是从sts返回来的数据里有<>这种标签,于是asp.net认为那是有危险的,于是抛出了异常,这个异常大家估计以前也碰到过,最简单粗暴的方法就是把验证请求的配置改为false,但这里我不建议这么干, 为此,我们专门用一个类来处理这种情况。 在SiteB目录下新建一个文件夹名为Services,然后添加一个类,名为SampleRequestValidator:   

  1. /// <summary>  
  2.   /// This SampleRequestValidator validates the wresult parameter of the  
  3.   /// WS-Federation passive protocol by checking for a SignInResponse message  
  4.   /// in the form post. The SignInResponse message contents are verified later by  
  5.   /// the WSFederationPassiveAuthenticationModule or the WIF signin controls.  
  6.   /// </summary>  
  7.   
  8.   
  9.   public class SampleRequestValidator : RequestValidator  
  10.   {  
  11.       protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)  
  12.       {  
  13.           validationFailureIndex = 0;  
  14.   
  15.   
  16.           if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal))  
  17.           {  
  18.                   return true;  
  19.           }  
  20.   
  21.   
  22.           return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);  
  23.       }  
  24.   
  25.   
  26.   }  
  /// <summary>
    /// This SampleRequestValidator validates the wresult parameter of the
    /// WS-Federation passive protocol by checking for a SignInResponse message
    /// in the form post. The SignInResponse message contents are verified later by
    /// the WSFederationPassiveAuthenticationModule or the WIF signin controls.
    /// </summary>


    public class SampleRequestValidator : RequestValidator
    {
        protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
        {
            validationFailureIndex = 0;


            if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal))
            {
                    return true;
            }


            return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
        }


    }

然后在web.config里加入这个类的配置:

  1. <httpRuntime targetFramework="4.5" requestValidationType="SiteC.Services.SampleRequestValidator" />  
    <httpRuntime targetFramework="4.5" requestValidationType="SiteC.Services.SampleRequestValidator" />

重新运行程序,非常完美:
这时候再打开SIteA,发现也已经处于了登录状态,这时候在SiteA点击退出,跳转到了登录页,再看看这时候的SiteB呢,刷新SiteB首页,发现也跳转到了登录页,证明在SiteA的退出操作对SiteB也起了作用,确实是单点登录了! SiteC和SiteD的配置与SiteB类似,这里我就不重复了,留给大家自己练习一下,等所有的项目都配置好以后,在任意站点登录,发现其它站点也是登录状态;在任意站点退出,发现其它站点也已经退出。利用WIF,单点登录变的如此简单~~

posted @ 2013-10-10 11:47  iDEAAM  阅读(874)  评论(0编辑  收藏  举报