首页 > 代码库 > 使用WIF实现单点登录Part IV —— 正式实战
使用WIF实现单点登录Part IV —— 正式实战
我们接下来的demo将包括以下的工程:
- SiteA —— 基于.net framework 4.5的MVC 4程序,使用WIF 4.5的SDK,第一个RP
- SiteB —— 基于.net framework 4.5的MVC 4程序,使用WIF 3.5的SDK,第二个RP
- SiteC —— 基于.net framework 4.0的MVC 4程序,使用WIF 3.5的SDK,第三个RP
- SiteD —— 基于.net framework 4.0 的WebApplication程序,使用WIF 3.5的SDK,第四个RP
- 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相关的配置全都删掉,因为我们都用不上。然后加上以下这两个节点:
- <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节点,不允许匿名用户访问站点:
- <authorization>
- <deny users="?"/>
- </authorization>
在system.webServer节点下增加2个HttpModule的配置节点:
- <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的配置节点:
- <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,将原有的代码删掉,改为如下代码:
- @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节点添加两个属性:
- <forms loginUrl="~/Account/Login" timeout="2880" slidingExpiration="true" name=".STSASPAUTH" />
在AppSettings里增加如下三个节点:
- <add key="IssuerName" value="PassiveSigninSTS" />
- <add key="SigningCertificateName" value="CN=localhost" />
- <add key="EncryptingCertificateName" value="" />
同样禁止匿名用户访问:
- <authorization>
- <deny users="?"/>
- </authorization>
在应用程序下新建一个名为Services的文件夹,在里面新建一个类文件,名为:CertificateUtil,用于获取证书,具体代码如下:
- 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,存放几个常量:
- public class Common
- {
- public const string IssuerName = "IssuerName";
- public const string SigningCertificateName = "SigningCertificateName";
- public const string EncryptingCertificateName = "EncryptingCertificateName";
- }
创建新类,名为SingleSignOnManager,用于注册RP以及获取RP列表:
- 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,用于返回需要的声明令牌:
- 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:
- 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()方法修改如下:
- 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)方法修改如下:
- [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方法修改如下:
- public ActionResult LogOff()
- {
- FormsAuthentication.SignOut();
- ViewData["AddressesExpected"] = SingleSignOnManager.SignOut().Distinct().ToArray();
- return View("Login");
- }
打开/Views/Account/Login.cshtml,添加以下代码:
- @{
- 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文件,添加几个站点:
- 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里添加如下节点:
- <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
Compilation里增加Microsoft.IdentityModel的程序集:
- <compilation debug="true" targetFramework="4.5" >
- <assemblies>
- <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
- </assemblies>
- </compilation>
身份验证改为None,添加authorization节点,禁止匿名用户访问:
- <authentication mode="None">
- </authentication>
- <authorization>
- <deny users="?" />
- </authorization>
添加三个httpModules:
- <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节点:
- <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和生日:
- @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:
- /// <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里加入这个类的配置:
- <httpRuntime targetFramework="4.5" requestValidationType="SiteC.Services.SampleRequestValidator" />