SSO解決方案大全
前段時間為我們的系統(tǒng)做SSO(單點登錄)參考了很多資料,其中包括博客園二級域名的登錄.
Single Sign-On (SSO)是近來的熱門話題. 很多和我交往的客戶中都有不止一個運行在.Net框架中的Web應用程序或者若干子域名.而他們甚至希望在不同的域名中也可以只登陸一次就可以暢游所有站點.今天我們關(guān)注的是如何在各種不同的應用場景中實現(xiàn) SSO. 我們由簡到繁,逐一攻破.
1. 虛擬目錄的主應用和子應用間實現(xiàn)SSO
2. 使用不同驗證機制實現(xiàn)SSO (username mapping)
3. 同一域名中,子域名下的應用程序間實現(xiàn)SSO
4. 運行在不同版本.NET下的應用程序間實現(xiàn)SSO
5. 兩個不同域名下的Web應用程序間實現(xiàn)SSO
6. 混合身份驗證方式模式 (Forms and Windows)下實現(xiàn)SSO
1. 虛擬目錄的主應用和子應用之間實現(xiàn)SSO
假設(shè)有兩個.Net的Web應用程序-Foo和Bar,Bar運行在Foo虛擬目錄的子目錄(http://foo.com/bar).二者都實現(xiàn)了Forms認證.實現(xiàn)Forms認證需要我們重寫Application_AuthenticateRequest,在這個時機我們完成認證一旦通過驗證就調(diào)用一下FormsAuthentication.RedirectFromLoginPage.這個方法接收的參數(shù)是用戶名或者其它的一些身份信息.在Asp.net中登錄用戶的狀態(tài)是持久化存儲在客戶端的cookie中.當你調(diào)用RedirectFromLoginPage時就會創(chuàng)建一個包含加密令牌FormsAuthenticationTicket的cookie,cookie名就是登錄用戶的用戶名.下面的配置節(jié)在Web.config定義了這種cookie如何創(chuàng)建:
比較重要的兩個屬性是 name 和protection.按照下面的配置就可以讓Foo和Bar兩個程序在同樣的保護級別下讀寫Cookie,這就實現(xiàn)了SSO的效果:
當 protection屬性設(shè)置為 "All",通過Hash值進行加密和驗證數(shù)據(jù)都存放在Cookie中.默認的驗證和加密使用的Key都存儲在machine.config文件,我們可以在應用程序的Web.Config文件覆蓋這些值.默認值如下:
IsolateApps表示為每個應用程序生成不同的Key.我們不能使用這個.為了能在多個應用程序中使用相同的Key來加密解密cookie,我們可以移除IsolateApps 選項或者更好的方法是在所有需要實現(xiàn)SSO的應用程序的Web.Config中設(shè)置一個具體的Key值:
如果你使用同樣的存儲方式,實現(xiàn)SSO只是改動一下Web.config而已.
2.使用不同認證機制實現(xiàn)SSO (username mapping)
要是FOO站點使用database來做認證,Bar站點使用Membership API或者其它方式做認證呢?這種情景中FOO站點創(chuàng)建的cookie對Bar站點毫無用處,因為cookie中的用戶名對Bar沒有什么意義.
要想cookie起作用,你就需要再為Bar站點創(chuàng)建一個認證所需的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站點創(chuàng)建了令牌FormsAuthenticationTicket ,這時令牌里的用戶名在Bar站點的上下文中就是有意義的了. 這時再調(diào)用 RedirectFromLoginPage創(chuàng)建正確的認證cookie.上面的例子你統(tǒng)一了了Forms 認證的cookie名字,而這里你要確保他們不同--因為我們不需要兩個站點共享相同的cookie:
現(xiàn)在當用戶在Foo站點登錄,他就會被映射到到Bar站點的用戶并同時創(chuàng)建了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的
3. 同一域名中,各子域名下應用程序間實現(xiàn)SSO
要是這樣的情況又將如何:Foo Bar兩個站點運行在不同的域名下: http://foo.com and http://bar.foo.com. 上面的代碼又不起作用了:因為cookie會存儲在不同的文件中,各自的cookie對其它網(wǎng)站不可見.為了能讓它起作用我們需要創(chuàng)建域級cookie,因為域級cookie對子域名都是可見的!這里我們也不能再使用 RedirectFromLoginPage 方法了,因為它不能靈活的創(chuàng)建域級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.com 和 http://bar.foo.com 以及其它子域名都是可見的.(譯者注:cookie的域名匹配規(guī)則是從右到左) .你可以通過設(shè)置Bar站點的認證cookie的域名為"bar.foo.com".這樣對于其它子域名的站點它的cookie也是不可見的,這樣安全了.注意 RFC 2109 要求cookie前面有兩個周期所以我們添加了一個過期時間.(cookie值實際上是一個字符串,各參數(shù)用逗號隔開).
再次提醒,這里還是需要統(tǒng)一一下各個站點的Web.config的
4. 運行在不同版本.Net下應用程序間實現(xiàn)SSO
要是Foo和Bar站點運行在不同的.Net環(huán)境中上面的例子都行不通.這是由于Asp.net 2.0使用了不同于1.1的加密算法:1.1版本使用的是3DES,2.0是AES.萬幸,Asp.net2.0中有一個屬性可以兼容1.1:
設(shè)置decryption="3DES"就會讓 ASP.NET 2.0使用舊版本的加密算法使cookie能夠正常使用.不要企圖在Asp.net1.1的Web.config文件中添加這個屬性,那會報錯.
5. 兩個不同域名下的應用程序?qū)崿F(xiàn)SSO
我們已經(jīng)成功的創(chuàng)建了可以共享的認證Cookie,但是如果Foo站點和Bar站點在不同域名下呢,例如: http://foo.com 和 http://bar.com? 他們不能共享cookie也不能為對方在創(chuàng)建一個可讀的cookie.這種情況下每個站點需要創(chuàng)有各自的cookie,調(diào)用其它站點的頁面來驗證用戶是否登錄.其中一種實現(xiàn)方式就是使用一系列的重定向.
為了實現(xiàn)上述目標,我們需要在每個站點都創(chuàng)建一個特殊的頁面(比如:sso.aspx).這個頁面的作用就是來檢查該域名下的cookie是否存在并返回已經(jīng)登錄用戶的用戶名.這樣其它站點也可以為這個用戶創(chuàng)建一個cookie了.下面是Bar.com的sso.aspx:
Bar.com:
這個頁面總是重定向回調(diào)用的站點.如果Bar.com存在認證cookie,它就解密出來用戶名放在ssoauth參數(shù)中.
另外一端(Foo.com),我們需要在HTTP Rquest處理的管道中添加一些的代碼.可以是Web應用程序的 Application_BeginRequest 事件或者是自定義的HttpHandler或HttpModule.基本思想就是在所有Foo.com的頁面請求之前做攔截,盡早的檢查驗證cookie是否存在:
1. 如果Foo.com的認證cookie已經(jīng)存在,就繼續(xù)處理請求,用戶在Foo.com登錄過
2. 如果認證Cookie不存在就重定向到Bar.com/sso.aspx.
3. 如果現(xiàn)在的請求是從Bar.com/sso.aspx重定向回來的,分析一下ssoauth參數(shù)如果需要就創(chuàng)建認證cookie.
路子很簡單,但是又兩個地方要注意死循環(huán):
// 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應用程序的有不同的
有些人把在url里面把用戶名當作參數(shù)傳遞視為畏途.實際上有兩件事情可以做來保護:首先我們可以檢查引用頁參數(shù)不接受bar.com/sso.aspx (or foo.com/ssp.aspx)以外的站點.其次,用戶名可以可以通過相同的Key做一下加密.如果Foo和Bar使用不同的認證機制,額外的用戶信息(比如email地址)同樣也可以傳遞過去.
6. 混合身份驗證模式下 (Forms and Windows)實現(xiàn)SSO
上面我們都是處理的Forms認證.要是我們這樣設(shè)計認證過程呢:先做Forms認證,如果沒有通過就檢查Intranet用戶是否已經(jīng)在NT域上登錄過了.這個思路我們需要檢查下面的參數(shù)來看和請求關(guān)聯(lián)的Windows logo信息:
Request.ServerVariables["LOGON_USER"]
但是除非我們的站點都是禁用匿名登錄的,否則這個值總是空的.我們可以在IIS的控制面板禁用匿名登錄并為我們的站點啟用Windows集成認證.這樣LOGON_USER 值就包含了NT域登錄用戶的名字.但是所有Internet用戶的都會遇到用戶名和密碼的難題,這就不好了,我們要讓Internet用戶使用Forms認證要是這種方式失敗了再使用Windows域認證.
這個問題的解決方法之一就是為Intranet用戶設(shè)置一個特殊的入口頁面:Windows集成認證方式可用,驗證域用戶,創(chuàng)建Forms cookie重定向到主站點.我們甚至可以隱藏這樣一個事實:由于Server.TransferIntranet用戶實際上訪問了不同的頁面.
也有一個簡單的解決方法.這個方法的基礎(chǔ)是IIS掌控認證處理.如果站點對匿名用戶可用,IIS就把請求傳遞給Asp.net運行時.并試圖進行認證要是失敗了就引發(fā)一個401錯誤.IIS會試圖尋找另外該站點的其它認證方式 .你要設(shè)置匿名訪問和集成認證可用并在Forms認證失敗之后執(zhí)行下面的代碼:
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!
}
這段代碼執(zhí)行時,它會檢查域用戶并取得一個空的初始值.這回終止當前請求并返回認證的401錯誤到IIS.這就讓IIS自動選擇另外的認證機制,Windows集成認證方式就是候選方式.如果用戶可以登錄到域,請求就可以繼續(xù),并附加上了NT域用戶的信息. 如果用戶沒有在域中登錄會有三次輸入用戶名密碼的機會.如果三次失敗他就會得到一個403錯誤(AccessDenied).
結(jié)論
我們考查了在各種場景中在兩個Asp.net應用程序間實現(xiàn)SSO.我們也可以在不同系統(tǒng)不同平臺間實現(xiàn)SSO,思想都是一樣的,只不過實現(xiàn)起來需要創(chuàng)造性思維.
最后說明下:用FormsAuthenticationTicket創(chuàng)建票據(jù)寫入cookies后如果退出認證的時候只用
FormsAuthentication.SignOut();
FormsAuthentication.RedirectToLoginPage();
是無法退出認證的,刷新以后cookies照樣存在(可能是我的方法不對。)
最后我用了一下方法成功退出。
FormsAuthentication.SignOut();
HttpCookie cookie = Request.Cookies[FormsAuthentication.FormsCookieName];
cookie.Expires = DateTime.Now.AddDays(-2);
cookie.Domain = "xxxx.cn ";
cookie.Path = FormsAuthentication.FormsCookiePath;
HttpContext.Current.Response.Cookies.Add(cookie);
Response.Clear();
FormsAuthentication.RedirectToLoginPage();
源文檔