SSO英文全稱Single Sign On,單點登陸是在多個應用系統中,使用者隻需要登入一次就可以通路所有互相信任的應用系統。它包括可以将這次主要的登入映射到其他應用中用于同一個使用者的登入的機制。它是目前比較流行的企業業務整合的解決方案之一。
SSO技術實作機制
當使用者第一次通路應用系統1的時候,因為還沒有登入,會被引導到認證系統中進行登入;根據使用者提供的登入資訊,認證系統進行身份效驗,如果通過效驗,應該傳回給使用者一個認證的憑據--ticket;使用者再通路别的應用的時候就會将這個ticket帶上,作為自己認證的憑據,應用系統接受到請求之後會把ticket送到認證系統進行效驗,檢查ticket的合法性。如果通過效驗,使用者就可以在不用再次登入的情況下通路應用系統2和應用系統3了。
要實作SSO,需要以下主要的功能:
1、所有應用系統共享一個身份認證系統
統一的認證系統是SSO的前提之一。認證系統的主要功能是将使用者的登入資訊和使用者資訊庫相比較,對使用者進行登入認證;認證成功後,認證系統應該生成統一的認證标志(ticket),返還給使用者。另外,認證系統還應該對ticket進行效驗,判斷其有效性。
2、所有應用系統能夠識别和提取ticket資訊
要實作SSO的功能,讓使用者隻登入一次,就必須讓應用系統能夠識别已經登入過的使用者。應用系統應該能對ticket進行識别和提取,通過與認證系統的通訊,能自動判斷目前使用者是否登入過,進而完成單點登入的功能。
另外:
1、單一的使用者資訊資料庫并不是必須的,有許多系統不能将所有的使用者資訊都集中存儲,應該允許使用者資訊放置在不同的存儲中。事實上,隻要統一認證系統,統一ticket的産生和效驗,無論使用者資訊存儲在什麼地方,都能實作單點登入。
2、統一的認證系統并不是說隻有單個的認證伺服器
認證伺服器之間要通過标準的通訊協定,互相交換認證資訊,就能完成更進階别的單點登入。如:當使用者在通路應用系統1時,由第一個認證伺服器進行認證後,得到由此伺服器産生的ticket。當他通路應用系統2的時候,認證伺服器2能夠識别此ticket是由第一個伺服器産生的,通過認證伺服器之間标準的通訊協定(例如SAML)來交換認證資訊,仍然能夠完成SSO的功能。
WEB-SSO的實作
使用者在通路頁面1的時候進行了登入,但是用戶端的每個請求都是單獨的連接配接,當客戶再次通路頁面2的時候,如何才能告訴Web伺服器,客戶剛才已經登入過了呢?浏覽器和伺服器之間有約定:通過使用cookie技術來維護應用的狀态。Cookie是可以被Web伺服器設定的字元串,并且可以儲存在浏覽器中。當浏覽器通路了頁面1時,web伺服器設定了一個cookie,并将這個cookie和頁面1一起傳回給浏覽器,浏覽器接到cookie之後,就會儲存起來,在它通路頁面2的時候會把這個cookie也帶上,Web伺服器接到請求時也能讀出cookie的值,根據cookie值的内容就可以判斷和恢複一些使用者的資訊狀态。Web-SSO完全可以利用Cookie結束來完成使用者登入資訊的儲存,将浏覽器中的Cookie和上文中的Ticket結合起來,完成SSO的功能。
為了完成一個簡單的SSO的功能,需要兩個部分的合作:
1、統一的身份認證服務。
2、修改Web應用,使得每個應用都通過這個統一的認證服務來進行身份效驗。
很多的網站都有用到SSO技術,
新浪的使用者登入也是用到的SSO技術.
我的想法是使用集中驗證方式,多個站點集中Passport驗證。
主站:Passport集中驗證伺服器 http://www.passport.com/ 。
分站:http://www.a.com/、http://www.b.com/、http://www.c.com/
憑證:使用者登入後産生的資料辨別,用于識别授權使用者,可為多種方式,DEMO中主站我使用的是Cache,分站使用Session。
令牌:由Passport頒發可在各分站中流通的唯一辨別。
OK,現在描述一下單點登入的過程:
情形一、匿名使用者:匿名使用者通路分站a上的一個授權頁面,首先跳轉到主站讓使用者輸入帳号、密碼進行登入,驗證通過後産生主站憑證,同時産生令牌,跳轉回分站a,此時分站a檢測到使用者已持有令牌,于是用令牌再次去主站擷取使用者憑證,擷取成功後允許使用者通路該授權頁面。同時産生分站a的本地憑證,當該使用者需要再次驗證時将先檢查本地憑證,以減少網絡互動。
情形二、在分站a登入的使用者通路分站b:因為使用者在分站a登入過,已持有令牌,是以分站b會用令牌去主站擷取使用者憑證,擷取成功後允許使用者通路授權頁面。同時産生分站b的本地憑證。
設計完成後,接下來是方案實作的一些關鍵點:
令牌:令牌由主站頒發,主站頒發令牌同時生成使用者憑證,并記錄令牌與使用者憑證之間的對應關系,以根據使用者提供的令牌響應對應的憑證;令牌要在各跨域分站中進行流通,是以DEMO中令牌我使用主站的Cookie,并指定Cookie.Domain="passport.com"。各分站如何共享主站的Cookie?從分站Redirect到主站頁面,然後該頁面讀取Cookie并以URL參數方式回傳即可,可在DEMO代碼中檢視詳細實作,當然如果哪位有更好的令牌實作方式也拿出來分享。
//産生令牌
string tokenValue = Guid.NewGuid().ToString().ToUpper();
HttpCookie tokenCookie = new HttpCookie("Token");
tokenCookie.Values.Add("Value", tokenValue);
tokenCookie.Domain = "passport.com";
Response.AppendCookie(tokenCookie);
主站憑證:主站憑證是一個關系表,包含了三個字段:令牌、憑證資料、過期時間。有多種實作方式可供選擇,要求可靠的話用資料庫,要求性能的話用Cache,DEMO中我使用的是Cache中的DataTable。如下代碼所示:
/// <summary>
/// 初始化資料結構
/// </summary>
/// <remarks>
/// ----------------------------------------------------
/// | token(令牌) | info(使用者憑證) | timeout(過期時間) |
/// |--------------------------------------------------|
/// </remarks>
private static void cacheInit()
{
if (HttpContext.Current.Cache["CERT"] == null)
{
DataTable dt = new DataTable();
dt.Columns.Add("token", Type.GetType("System.String"));
dt.Columns["token"].Unique = true;
dt.Columns.Add("info", Type.GetType("System.Object"));
dt.Columns["info"].DefaultValue = null;
dt.Columns.Add("timeout", Type.GetType("System.DateTime"));
dt.Columns["timeout"].DefaultValue = DateTime.Now.AddMinutes(double.Parse(System.Configuration.ConfigurationManager.AppSettings["timeout"]));
DataColumn[] keys = new DataColumn[1];
keys[0] = dt.Columns["token"];
dt.PrimaryKey = keys;
//Cache的過期時間為 令牌過期時間*2
HttpContext.Current.Cache.Insert("CERT", dt, null, DateTime.MaxValue, TimeSpan.FromMinutes(double.Parse(System.Configuration.ConfigurationManager.AppSettings["timeout"]) * 2));
}
}
分站憑證:分站憑證主要用于減少重複驗證時網絡的互動,比如使用者已在分站a上登入過,當他再次通路分站a時,就不必使用令牌去主站驗證了,因為分站a已有該使用者的憑證。分站憑證相對比較簡單,使用Session、Cookie均可。
分站SSO頁面基類:分站使用SSO的頁面會做一系列的邏輯判斷處理,如文章開頭的流程圖。如果有多個頁面的話不可能為每個頁寫一個這樣的邏輯,OK,那麼把這套邏輯封裝成一個基類,凡是要使用SSO的頁面繼承該基類即可。如下代碼所示:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Text.RegularExpressions;
namespace SSO.SiteA.Class
{
/// <summary>
/// 授權頁面基類
/// </summary>
public class AuthBase : System.Web.UI.Page
{
protected override void OnLoad(EventArgs e)
{
if (Session["Token"] != null)
{
//分站憑證存在
Response.Write("恭喜,分站憑證存在,您被授權通路該頁面!");
}
else
{
//令牌驗證結果
if (Request.QueryString["Token"] != null)
{
if (Request.QueryString["Token"] != "$Token$")
{
//持有令牌
string tokenValue = Request.QueryString["Token"];
//調用WebService擷取主站憑證
SSO.SiteA.RefPassport.TokenService tokenService = new SSO.SiteA.RefPassport.TokenService();
object o = tokenService.TokenGetCredence(tokenValue);
if (o != null)
{
//令牌正确
Session["Token"] = o;
Response.Write("恭喜,令牌存在,您被授權通路該頁面!");
}
else
{
//令牌錯誤
Response.Redirect(this.replaceToken());
}
}
else
{
//未持有令牌
Response.Redirect(this.replaceToken());
}
}
//未進行令牌驗證,去主站驗證
else
{
Response.Redirect(this.getTokenURL());
}
}
base.OnLoad(e);
}
/// <summary>
/// 擷取帶令牌請求的URL
/// 在目前URL中附加上令牌請求參數
/// </summary>
/// <returns></returns>
private string getTokenURL()
{
string url = Request.Url.AbsoluteUri;
Regex reg = new Regex(@"^.*/?.+=.+$");
if (reg.IsMatch(url))
url += "&Token=$Token$";
else
url += "?Token=$Token$";
return "http://www.passport.com/gettoken.aspx?BackURL=" + Server.UrlEncode(url);
}
/// <summary>
/// 去掉URL中的令牌
/// 在目前URL中去掉令牌參數
/// </summary>
/// <returns></returns>
private string replaceToken()
{
string url = Request.Url.AbsoluteUri;
url = Regex.Replace(url, @"(/?|&)Token=.*", "", RegexOptions.IgnoreCase);
return "http://www.passport.com/userlogin.aspx?BackURL=" + Server.UrlEncode(url);
}
}//end class
}
使用者退出:使用者退出時分别清空主站憑證與目前分站憑證。如果要求A站點退出,B、C站點也退出,可自行擴充接口清空每個分站憑證。
主站過期憑證/令牌清除:定時清除(DataTable)Cache[“CERT”]中timeout字段超過目前時間的記錄。
--------------------
系統的基本架構
我們假設一個系統System包含Service客戶服務中心、Shop網上購物中心和Office網上辦公中心三個獨立的網站。Service管理客戶的資料,登入和登出過程。不論客戶通路System的任何一個頁面,系統都會轉到登入界面,在使用者登入後,系統會自動轉會到客戶上次請求的頁面。并且使用者此後可以在System中無縫切換。不需要再次進行登入。即在System中實作單點登入SSO(Single Sign-On)。
我們知道,使用者的即時狀态通常是使用Application、Session、Cookie和存儲的。而這些都是不能在程式中跨站點通路的。我們必需通過站點間互相通訊來确認使用者的即時狀态。
簡單的實作
如圖所示,該圖描述了使用者通路System的流程。
第一步,假設使用者通路了Shop或Office的任何一個頁面Any。該頁面所在的網站将會檢查使用者的即時狀态。如果使用者已經登入了,則将Any頁面的資訊傳回給使用者。如果使用者還沒有登入,則自動轉到Service的Validate頁面,驗證使用者在Service狀态。即Shop或Office向Service送出請求,要求Service傳回使用者的即時狀态。
第二步,Validate驗證使用者的即時狀态,如果使用者已經登入了,則Service将使用者的即時狀态傳回給Shop或Office的同步頁面Synchronous,通知Shop或Office同步使用者狀态。如果使用者沒有登入,則自動轉向Customer頁面,提示使用者登入。
第三步,使用者完成登入過程,當使用者成功登入後,自動轉回Validate頁面,通知Shop或Office的Synchronous進行使用者狀态的同步。
第四步,在使用者狀态同步完成後,在本地站點,使用者狀态成為線上狀态,即可通路Any頁面。
在上面的流程中。我們知道,不管使用者通路哪個站點,使用者隻需要一次登入,就保證使用者在Service的即時狀态都是線上的,不會再需要進行第二次登入的過程。
現在我們的思路已經清楚,具體的實作我們将在代碼分析中完成。
代碼分析
從上面的流程中我們可以看出,系統中Shop和Office的代碼是完全類似的。隻要Shop可以實作,Office也可以同樣的克隆。是以我們的重點分析的對象是Shop和Service的代碼。
1、Shop的Web.config和Project.cs
在Shop的Web.config裡,我們配置了Service站點和Shop站點,以友善我們在部署時友善修改。
<appsettings>
<add key="Service" value="http://localhost:8001" />
<add key="WebSite" value="http://localhost:8002" />
</appsettings>
在Project類裡進行引用。
using System;
using System.Configuration;
namespace Amethysture.SSO.Shop
{
public class Project
{
public static string Service = ConfigurationSettings.AppSettings["Service"];
public static string WebSite = ConfigurationSettings.AppSettings["WebSite"];
}
}
2、Shop的Global.cs
Shop的Global.cs定義了四個Session變量,UserID用來辨別使用者身份。Pass辨別使用者即時狀态,Security用于儲存往來Service和Shop的通訊不是被仿冒的。Url儲存了上次請求的頁面,以保證在使用者登入後能轉到使用者請求的頁面。
protected void Session_Start(Object sender, EventArgs e)
{
this.Session.Add("UserID", 0);
this.Session.Add("Pass", false);
this.Session.Add("Security", "");
this.Session.Add("Url", "");
}
3、Shop的Any.cs
Shop的Any.cs并沒有包含代碼,因為Any類從Page繼承而來,為了代碼分析友善,我們将代碼集中到Page.cs中。
using System;
using System.Web;
namespace Amethysture.SSO.Shop
{
public class Any : Amethysture.SSO.Shop.Page
{
}
}
4、Shop的Page.cs
Page類有兩個方法,CustomerValidate和Initialize。CustomerValidate使用者檢查使用者的即時狀态,而Initialize是頁面登入後發送給使用者的資訊。我們的重點是CustomerValidate。
CustomerValidate是一個非常簡單的流程,用條件語句檢查Pass的狀态,如果Pass為否,則表示使用者沒有登入,頁面跳轉到Service的Validate頁面中。我們要分析的是其中儲存的Url和遞交的WebSite和Security幾個參數。Url的作用在前面已經講清楚了,隻是為了保證使用者登入後能回到原來的頁面。而WebSite是為了保證該站點是被Service所接受的,并且保證Service知道是哪個站點請求了使用者即時狀态。因為這個例子是個簡單的例子,在後面的Validate裡沒有驗證WebSite是否是接受的請求站點,但是在實際應用中應該驗證這一點,因為Shop和Service等同于伺服器和用戶端,伺服器出于安全考慮必須要檢查用戶端是否是被允許的。Security是非常重要的一點。Shop對Service發送的是請求,不需要保證該請求沒有被篡改,但是在Service應答Shop請求時就必須要保證應答的資料沒有被篡改了。Security正是為了保證資料安全而設計的。
在代碼中,Security是通過Hash一個随機産生的數字生成的。具有不确定性。和保密性。我們可以看到,Security同時儲存在Session中和發送給Service。我們把這個Security當作明文。在後面我們可以看到,Security在Service經過再一次Hash後作為密文發送回Shop。如果我們将Session儲存的Security經過同樣的Hash方法處理後等到的字元串如果和Service傳回的密文相同,我們就能夠在一定程度上保證Service應答的資料是沒有經過修改的。
using System;
using System.Web;
using System.Security.Cryptography;
using System.Text;
namespace Amethysture.SSO.Shop
{
public class Page : System.Web.UI.Page
{
private void CustomerValidate()
{
bool Pass = (bool) this.Session["Pass"];
if (!Pass)
{
string Security = "";
Random Seed = new Random();
Security = Seed.Next(1, int.MaxValue).ToString();
byte[] Value;
UnicodeEncoding Code = new UnicodeEncoding();
byte[] Message = Code.GetBytes(Security);
SHA512Managed Arithmetic = new SHA512Managed();
Value = Arithmetic.ComputeHash(Message);
Security = "";
foreach(byte o in Value)
{
Security += (int) o + "O";
}
this.Session["Security"] = Security;
this.Session["Url"] = this.Request.RawUrl;
this.Response.Redirect(Project.Service + "/Validate.aspx?WebSite=" + Project.WebSite + "&Security=" + Security);
}
}
protected virtual void Initialize()
{
this.Response.Write("<html>");
this.Response.Write("<head>");
this.Response.Write("<title>Amethysture SSO Project</title>");
this.Response.Write("<link rel=stylesheet type=/"text/css/" href=/"" + project.website + "/Default.css/">");
this.Response.Write("</head>");
this.Response.Write("<body>");
this.Response.Write("<iframe width=/"0/" height=/"0/" src=/"" + project.service + "/Customer.aspx/"></iframe>");
this.Response.Write("<div align=/"center/">");
this.Response.Write("Amethysture SSO Shop Any Page");
this.Response.Write("</div>");
this.Response.Write("</body>");
this.Response.Write("</html>");
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.CustomerValidate();
this.Initialize();
this.Response.End();
}
}
}
5、Service的Global.cs
現在我們頁面轉到了Service的Validate頁面,我們轉過來看Service的代碼。在Global中我們同樣定義了四個Session變量,都和Shop的Session用處類似。WebSite是儲存請求使用者即時狀态的站點資訊。以便能在登入後傳回正确的請求站點。
protected void Session_Start(Object sender, EventArgs e)
{
this.Session.Add("UserID", 0);
this.Session.Add("Pass", false);
this.Session.Add("WebSite", "");
this.Session.Add("Security", "");
}
6、Service的Validate.cs
首先,将Shop傳遞過來的參數儲存到Session中。如果使用者沒有登入,則轉到Customer頁面進行登入。如果使用者已經登入了。則将使用者即時狀态傳回給Shop站點。如上所述,這裡将Security重新Hash了一次傳回給Shop,以保證資料不被纂改。
private void CustomerValidate()
{
bool Pass = (bool) this.Session["Pass"];
if ((this.Request.QueryString["WebSite"] != null) && (this.Request.QueryString["WebSite"] != ""))
{
this.Session["WebSite"] = this.Request.QueryString["WebSite"];
}
if ((this.Request.QueryString["Security"] != null) && (this.Request.QueryString["Security"] != ""))
{
this.Session["Security"] = this.Request.QueryString["Security"];
}
if (Pass)
{
string UserID = this.Session["UserID"].ToString();
string WebSite = this.Session["WebSite"].ToString();
string Security = this.Session["Security"].ToString();
byte[] Value;
UnicodeEncoding Code = new UnicodeEncoding();
byte[] Message = Code.GetBytes(Security);
SHA512Managed Arithmetic = new SHA512Managed();
Value = Arithmetic.ComputeHash(Message);
Security = "";
foreach(byte o in Value)
{
Security += (int) o + "O";
}
this.Response.Redirect(WebSite + "/Synchronous.aspx?UserID=" + UserID + "&Pass=True&Security=" + Security);
}
else
{
this.Response.Redirect("Customer.aspx");
}
}
7、Service的Customer.cs和Login.cs
Customer主要的是一個用于登入的表單,這裡就不貼出代碼了。這裡分析一下Login的一段代碼,這段代碼是當登入是直接在Service完成的(WebSite為空值),則頁面不會轉到Shop或Office站點。是以應該暫停在Service站點。系統如果比較完美,這裡應該顯示一組字系統的轉向連結。下面我們看到,當Pass為真時,頁面轉回到Validate頁面,通過上面的分析,我們知道,頁面會轉向Shop的Synchronous頁面,進行使用者狀态的同步。
if (Pass)
{
if ((this.Session["WebSite"].ToString() != "") && (this.Session["Security"].ToString() != ""))
{
this.Response.Redirect("Validate.aspx");
}
else
{
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("Pass");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
}
}
else
{
this.Response.Redirect("Customer.aspx");
}
8、Shop的Synchronous.cs
好了,我們在Service中完成了登入,并把使用者狀态傳遞回Shop站點。我們接着看使用者狀态是怎麼同步的。首先,如果Session裡的Security是空字元串,則表示Shop站點沒有向Service發送過請求,而Service向Shop發回了請求,這顯然是錯誤的。這次通路是由用戶端僞造進行的通路,于是通路被拒絕了。同樣Security和InSecurity不相同,則表示請求和應答是不比對的。可能應答被纂改過了,是以應答同樣被拒絕了。當檢驗Security通過後,我們保證Serive完成了應答,并且傳回了确切的參數,下面就是讀出參數同步Shop站點和Service站點的使用者即時狀态。
string InUserID = this.Request.QueryString["UserID"];
string InPass = this.Request.QueryString["Pass"];
string InSecurity = this.Request.QueryString["Security"];
string Security = this.Session["Security"].ToString();
if (Security != "")
{
byte[] Value;
UnicodeEncoding Code = new UnicodeEncoding();
byte[] Message = Code.GetBytes(Security);
SHA512Managed Arithmetic = new SHA512Managed();
Value = Arithmetic.ComputeHash(Message);
Security = "";
foreach(byte o in Value)
{
Security += (int) o + "O";
}
if (Security == InSecurity)
{
if (InPass == "True")
{
this.Session["UserID"] = int.Parse(InUserID);
this.Session["Pass"] = true;
this.Response.Redirect(this.Session["Url"].ToString());
}
}
else
{
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("資料錯誤");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
}
}
else
{
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("通路錯誤");
this.Response.Write("");
this.Response.Write("");
this.Response.Write("");
}
9、Shop的Page.cs
我們知道,頁面在一段時間不重新整理後,Session會逾時失效,在我們一直通路Shop的時候怎麼才能保證Service的Session不會失效呢?很簡單,我們傳回來看Shop的Page.cs。通過在所有Shop的頁面内都用<iframe>嵌套Service的某個頁面,就能保證Service能和Shop的頁面同時重新整理。需要注意的一點是Service的Session必須保證不小于所有Shop和Office的Session逾時時間。這個在Web.config裡可以進行配置。
this.Response.Write("<iframe width=/"0/" height=/"0/" src=/"" + project.service + "/Customer.aspx/"></iframe>");
總結
一次完整的登入完成了。我們接着假設一下現在要跳到Office的Any頁面,系統會進行怎樣的操作呢?Any(使用者沒有登入)->Validate(使用者已經登入)->Synchronous(同步)->Any。也就是說這次,使用者沒有進行登入的過程。我們通過一次登入,使得Service的使用者狀态為登入,并且不管有多少個網站應用,隻要這些網站都保證符合Shop的特性,這些網站就都能保持Service的使用者狀态,同時能通過Service獲得使用者的狀态。也就是說我們實作了SSO。