資料參考來源 : 我姓區不姓區html
有關於WIF的介紹以及環境配置在此很少說,能夠去網上搜索,或者點擊上方連接前往查看,如下所述都基於WIF配置完成的條件上;web
如下不少東西都是從 我姓區不姓區 的博客直接copy過來的,我另外加的就是我跟着他的博客一路中所踩的坑以及我本身的理解;瀏覽器
開始單點登陸踩坑之旅:服務器
咱們接下來的demo將包括如下的工程:cookie
1、建立第一個RPsession
以管理員身份打開vs2012,在起始頁上點擊「新建項目」,在左邊的「模板」樹下,展開「其它項目類型」,而後選擇「Visual Studio解決方案」,「名稱」輸入框裏輸入WIFSSO,而後選擇解決方案的路徑後點擊」肯定「,如圖:app
在」解決方案資源管理器「中,在新建好的解決方案上點右鍵,選擇」添加「->」新建項目「。在彈出的對話框中選擇」ASP.NET MVC 4 Web應用程序「,記得.Net Framework版本選4.5,名稱起名爲」SiteA「,而後點肯定,如圖:asp.net
在彈出的「新ASP.NET MVC 4項目」對話框中直接點「肯定」,第一個RP項目新建完成後,添加如下兩個引用:System.IdentityModel和System.IdentityModel.Services。此次的教程不使用Identity and Access Tool,而是直接修改web.config文件,這樣能使你們對WIF的配置有更深刻的瞭解。ide
打開web.config文件,將configSections節裏的entityFramework配置節點刪掉,由於咱們不須要用到Entity Framework。最好把web.config中關於Entity Framework相關的配置全都刪掉,由於咱們都用不上。而後加上如下這兩個節點:post
<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的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文件,添加幾個站點:
注意:更改host文件須要管理員權限,不然是改動不了的;這個更改的做用是:將域名指向的網址變成本地,有喜歡惡做劇的朋友能夠把別人最喜歡的域名網站指向到本地或者其餘網站等等,哈哈,不知道這個東西的人會懵比的,哈哈;
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:
點擊退出,將註銷當前用戶,並跳轉到登陸頁。
注意:以上是原博客中的原文,我在實踐的過程當中曾報出一個問題:
錯誤:X.509 證書 CN=localhost 不在被信任的人的存儲中。 X.509 certificate CN=localhost 鏈生成失敗。所使用的證書具備沒法驗證的信任鏈。請替換該證書或更改 certificateValidationMode。已處理證書鏈,可是在不受信任提供程序信任的根證書中終止;
開始看到這個問題是懵逼的,在網上搜索了很久都沒找到答案,多般曲折,最終仍是找到了,http://www.cnblogs.com/pangguoming/p/5833009.html。我將localhost證書導出,而後在導入到受信任的根證書頒發機構,文件名寫CN=後面的東西,而後運行成功;
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" />
從新運行程序,很是完美:
這時候再打開SIteA,發現也已經處於了登陸狀態,這時候在SiteA點擊退出,跳轉到了登陸頁,再看看這時候的SiteB呢,刷新SiteB首頁,發現也跳轉到了登陸頁,證實在SiteA的退出操做對SiteB也起了做用,確實是單點登陸了!
SiteC和SiteD的配置與SiteB相似,這裏我就不重複了,留給你們本身練習一下,等全部的項目都配置好之後,在任意站點登陸,發現其它站點也是登陸狀態;在任意站點退出,發現其它站點也已經退出。利用WIF,單點登陸變的如此簡單~~
當我按照上面的教程完成了以後,確實可以實現了單點登陸,可是我產生了如下幾個疑惑: