不好意思,最近一段时间真是太忙了,导致一直没有更新,有好几位朋友都表示让我赶紧接着往下写,偶心里真是挺感动~~
好了,经过前两篇文章,估计大家对WIF已经有比较充分的认识了,估计大家在经过了枯燥的理论以后,肯定是摩拳擦掌赶紧付诸于行动了。没办法,咱们程序员就是这个毛病。那好吧,我也不那么多废话了,直接进入正题吧。
我们接下来的demo将包括以下的工程:
以管理员身份打开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" />
<authorization> <deny users="?"/> </authorization>
<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>
<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>
至此,SiteA就已经完成了。你是不是迫不及待的想要运行了呢?别急,虽然有SiteA了,但还没有STS呢,现在启动SiteA,由于没登录,所以它会跳转到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>
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"; }
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); } } }
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; } }
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; } } } }
public ActionResult Index() { FederatedPassiveSecurityTokenServiceOperations.ProcessRequest( System.Web.HttpContext.Current.Request, User as ClaimsPrincipal, CustomSecurityTokenServiceConfiguration.Current.CreateSecurityTokenService(), System.Web.HttpContext.Current.Response); return View(); }
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Login(LoginModel model, string returnUrl) { var query = HttpUtility.ParseQueryString(Request.UrlReferrer.Query); if (model.UserName == "[email protected]" && model.Password == "123456") { FormsAuthentication.SetAuthCookie("[email protected]|1983-10-22|oujian", false); if (!string.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); return RedirectToAction("Index", "Home"); } return View(model); }
public ActionResult LogOff() { FormsAuthentication.SignOut(); ViewData["AddressesExpected"] = SingleSignOnManager.SignOut().Distinct().ToArray(); return View("Login"); }
@{ 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的登录页面,输入[email protected],密码123456,确定,登录成功,跳回到了www.sitea.com,并显示出了用户名和Email:
点击退出,将注销当前用户,并跳转到登录页。
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<compilation debug="true" targetFramework="4.5" > <assemblies> <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> </assemblies> </compilation>
<authentication mode="None"> </authentication> <authorization> <deny users="?" /> </authorization>
<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> <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>
@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>
/// <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); } }
<httpRuntime targetFramework="4.5" requestValidationType="SiteC.Services.SampleRequestValidator" />