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