SSO英文全稱Single Sign On,單點登錄。SSO是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。它包括可以將這次主要的登錄映射到其他應用中用於同一個用戶的登錄的機制。它是目前比較流行的企業業務整合的解決方案之一
現在很多企業級應用都基本會去實現單點登陸功能,這樣對於用戶體驗上會有不錯的加強。不需要重復登陸多次。
如上圖所示,整個SSO的實現最重要就是SSO服務器的實現形式。很多SSO都是自己編寫服務來實現!在登陸的時候,一般都在電腦上取出一種唯一標識然后保存在SSO服務器,以這唯一標識去識別是否已經登陸!這是跨域的一種實現形式!
今天我所以說的簡要示例,並不是跨域SSO實現,是在同一頂級域名下的SSO實現,因為在整個示例中都是站點為服務器!此示例的意義在於詳述相關的基本實現原理,並不是要說一些多么深奧知識!下面大概陳述一下一些關鍵代碼.
一:首先子站需要實現的是兩部份,一個是獲取到SSO服務器站點發送過來的相關信息,包括登陸后的TOKEN票據(加密)!一個是登出操作的頁面,由SSO服務器調用后,負責清除本站點的登陸狀態。

using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using Fdays.SSO.Lib.Client.CryptoClass; using Fdays.SSO.Lib.Client.Model; using System.Configuration; namespace Fdays.NOTMVCSite { public partial class GetServicePost : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { if (Request["Plat"] != null) { string plat = Request["Plat"].ToString(); MO_Request request = CryptoServices.Decrypt(plat, Convert.ToInt32(ConfigurationManager.AppSettings["AppCode"])); if (Request["LogoutResult"] == null) { Session["Token"] = request.Token; } else { string logoutResult = Request["LogoutResult"].ToString(); } Response.Redirect(request.UserUrl); } } } } }

using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace Fdays.NOTMVCSite { public partial class SSOLogout : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { //必要操作 Session["Token"] = null; //站點登出時所需操作 //Session["Uid"] = null; //Session["Cid"] = null; //UMSvc.Account.Logout(MSContext); } } } }
二:訪問子站點的時候需要做作狀態判斷,如果未登陸時,將跳轉到SSO服務器登陸。例子中用的是方法三,將站點代號、用戶訪問頁面、子站點與SSO服務器交互的地址發送到SSO服務器,方法中還有一個用於同步SSO服務器與子站COOKIE過期時間的方法(涉及到狀態同步)!

using System; using System.Collections.Generic; using System.Linq; using System.Web; using Fdays.SSO.Lib.Client; using System.Configuration; namespace Fdays.NOTMVCSite { public class SSOPage: System.Web.UI.Page { protected override void OnLoad(EventArgs e) { Filter(); base.OnLoad(e); } private static void Filter() { if (HttpContext.Current.Session["Token"] == null) { //方法1 //SiteOperate.Login(Convert.ToInt32(ConfigurationManager.AppSettings["AppCode"])); //方法2 //SiteOperate.Login(Convert.ToInt32(ConfigurationManager.AppSettings["AppCode"]), "http://localhost:5005/Test2.aspx"); //方法3 SiteOperate.Login(Convert.ToInt32(ConfigurationManager.AppSettings["AppCode"]), "http://localhost:5005/Test2.aspx", "http://localhost:5005/Site/GetServicePost"); } else if (Authentication.isSetTokenTime(1)) //更新Token過期時間 { Authentication.SetTokenExpiredTime(); } } } }
三:服務器需要做的是獲取子站點的登陸信息(解密),進行登陸,登陸完畢后需要將生成的DUID形式唯一票據TOKEN,發送回子站點!!登陸完畢后,此時有兩種形式保存用戶信息,1.保存在COOKIE(涉及到安全問題)2.緩存服務器memcached。如果子站需要獲取用戶信息就需要再次與SSO服務器或緩存服務器做交互!以下為部份代碼:

#region 【獲取子站發送的Post數據】 /// <summary> /// 【FUNC】: 獲取子站發送的Post數據 /// 【NAME】: 李建標 /// 【TIME】: 2010-01-12 10:50:00 /// </summary> //[HttpPost] public void GetClientPost() { RSACryption rsaCryption = new RSACryption(); MO_Request request = new MO_Request(); try { #region 獲取請求數據 if (!string.IsNullOrEmpty(Request.QueryString["Plat"]) && !string.IsNullOrEmpty(Request.QueryString["AppCode"])) { string appCode = Request.QueryString["AppCode"].ToString(); Authentication.AddSite(appCode); request = CryptoServices.Decrypt(Request.QueryString["Plat"].ToString(), Convert.ToInt32(appCode)); SSOCookie.SaveCookie("MO_request", DataConversion.getStrRequest(request), DateTime.Now.AddMinutes(30), "/"); SSOCookie.SaveCookie("AppCode", appCode.ToString(), DateTime.Now.AddMinutes(30), "/"); } #endregion #region 判斷是否請求登出 if (!string.IsNullOrEmpty(Request.QueryString["Logout"])) { TempData["Logout"] = Request["Logout"]; ClientLogoutProc(); return; } #endregion #region 注釋 //Authentication.AddSite(Request["AppCode"]); //if (!string.IsNullOrEmpty(Request["Plat"]) && !string.IsNullOrEmpty(Request["AppCode"])) //{ // request = CryptoServices.Decrypt(Request["Plat"].ToString(), Convert.ToInt32(Request["AppCode"])); // SSOCookie.SaveCookie("MO_request", DataConversion.getStrRequest(request), DateTime.Now.AddMinutes(30), "/"); // SSOCookie.SaveCookie("AppCode", Request["AppCode"], DateTime.Now.AddMinutes(30), ""); //} //#endregion //#region 判斷是否請求登出 //if (!string.IsNullOrEmpty(Request["Logout"])) //{ // TempData["Logout"] = Request["Logout"]; // ClientLogoutProc(); // return; //} #endregion #region 判斷是否已經登錄 if (SSOCookie.IsExist("Token")) { if (String.IsNullOrEmpty(request.Token)) { ClientLoginProc(new Guid(SSOCookie.GetCookie("Token"))); } else { GetTandSetCookie(request); } return; } else if (!string.IsNullOrEmpty(request.Token)) //沒有登錄 { SSOCookie.SaveCookie("Token", request.Token, DateTime.Now.AddMinutes(30), "/"); GetTandSetCookie(request); return; } //if(UMUser.Uid != null) //{ // UMSvc.Logs.Debug(Fdays.SSO.Model.Aaron.Emun.LogType.LGUSEROPT, UMUser.Uid, "獲取子站發送的Post數據成功!"); //} #endregion } catch (InvalidCastException icx)//數據轉換出錯 { UMSvc.Account.Logout(MSContext); UMSvc.Logs.Debug(Fdays.SSO.Model.Aaron.Emun.LogType.LGSSERROR, UMUser.Uid, "獲取子站發送的Post數據失敗 --- 數據轉換出錯!:" + icx.Message); Jscript.AlertAndRedirect("操作失敗,請重新登錄!", "Login"); } catch (Exception ex) { UMSvc.Account.Logout(MSContext); UMSvc.Logs.Debug(Fdays.SSO.Model.Aaron.Emun.LogType.LGSSERROR, UMUser.Uid, "獲取子站發送的Post數據失敗:" + ex.Message); Jscript.AlertAndRedirect("操作失敗,請重新登錄!", "Login"); } finally { } Response.Redirect("~/Authen/login"); } #endregion

#region 【從子站請求登錄處理】 /// <summary> /// 【FUNC】:處理由子站發錄登錄請求 /// 【NAME】:李建標 /// 【TIME】:2010-02-12 10:48:00 /// </summary> /// <param name="token">票據Guid</param> public void ClientLoginProc(Guid token) { bool isOk = false; try { PostProcess postProcess = new PostProcess(); MO_Request request = DataConversion.getMORequest(SSOCookie.GetCookie("MO_request")); request.Token = token.ToString(); postProcess.Url = request.CallbackUrl; postProcess.Add("Plat", CryptoServices.Encrypt(request, Convert.ToInt32(SSOCookie.GetCookie("AppCode")))); postProcess.Add("Request", CryptoServices.EncryptRStr(SSOCookie.GetTCookie(token.ToString()), Convert.ToInt32(SSOCookie.GetCookie("AppCode")))); SSOCookie.DelCookie("MO_request"); SSOCookie.DelCookie("AppCode"); postProcess.PostToClient(); isOk = true; } catch (Exception ex) { UMSvc.Logs.Debug(Fdays.SSO.Model.Aaron.Emun.LogType.LGSSERROR, UMUser.Uid, "處理從子站登錄時的數據錯誤:" + ex.Message); isOk = false; } finally { if (isOk != true) { UMSvc.Account.Logout(MSContext); Jscript.AlertAndRedirect("數據出錯,請重新登錄!", "Login"); } } } #endregion

/// <summary> /// 【FUNC】:將驗證數據回傳到子戰點 /// 【作得】:李建標 /// 【TIME】:2010-02-12 14:11:00 /// </summary> public void PostToClient() { System.Web.HttpContext.Current.Response.Clear(); StringBuilder sbPostHtml = new StringBuilder(); sbPostHtml.AppendFormat("<html>"); sbPostHtml.AppendFormat("<head></head>"); sbPostHtml.AppendFormat("<body onload=\"document.{0}.submit()\">", FormName); sbPostHtml.AppendFormat("<form name=\"{0}\" method=\"post\" action=\"{2}\">", FormName, Method, Url); try { foreach (string keys in Inputs) { sbPostHtml.AppendFormat("<input name=\"{0}\" type=\"hidden\" value=\"{1}\">", keys, Inputs[keys]); } sbPostHtml.AppendFormat("</form>"); sbPostHtml.AppendFormat("</body>"); sbPostHtml.AppendFormat("</html>"); System.Web.HttpContext.Current.Response.Write(sbPostHtml.ToString()); System.Web.HttpContext.Current.Response.End(); } catch (Exception) { //輸入異常提示 } finally { } //StringBuilder sbCondition = new StringBuilder(); //sbCondition.Append("?Sign=100"); //foreach (string keys in Inputs) //{ // sbCondition.AppendFormat("&{0}={1}", keys, Inputs[keys]); //} //System.Web.HttpContext.Current.Response.Redirect(Url + sbCondition.ToString()); //System.Web.HttpContext.Current.Response.End(); }
四:在子站點A跳轉到子站點B,同樣判斷登陸狀態!然后跳轉到SSO服務器,因為登陸狀態還是存在,所以服務器不需要再作登陸,只需要將保存的TOKEN直接發送到子站點B就可以了,而服務器需要做的工作就是,在已登陸站點中增加子站點B到站點隊列中(做登出操作的時候用到)!相關代碼與一、二一樣,此處不做展示!
五:子站點拿出操作時,站點將帶着TOKEN與站點代碼跳轉到服務器!服務器接受到子站點請求后,將清理本身保存的COOKIE與SESSION。然后遍歷已登陸站點列表,發出拿出請求;

//站點向服務器發出登出請求(子站點代碼) public void Logout() { //方法1 SiteOperate.Logout(appCode, Session["Token"].ToString()); //方法2 //SiteOperate.Logout(appCode, Session["Token"].ToString(), "http://localhost:5002/Site/Test2"); //方法3 //SiteOperate.Logout(appCode, Session["Token"].ToString(), "http://localhost:5002/Site/Test2", "http://localhost:5002/Site/GetServicePost"); }

#region 【從子站請求登出處理】 /// <summary> /// 【FUNC】: 處理子站登出請求 /// 【NAME】: 李建標 /// 【TIME】: 2010-02-12 10:44:00 /// </summary> public void ClientLogoutProc() { bool isOk = false; string Err = ""; MO_Request request = new MO_Request(); PostProcess postProcess = new PostProcess(); try { string strRequest = SSOCookie.GetCookie("MO_request"); request = DataConversion.getMORequest(strRequest); postProcess.Url = request.CallbackUrl; postProcess.Add("Plat", CryptoServices.Encrypt(request, Convert.ToInt32(SSOCookie.GetCookie("AppCode")))); postProcess.Add("LogoutResult", "登出成功!"); isOk = true; } catch (Exception ex) { Err = ex.StackTrace; UMSvc.Logs.Debug(Fdays.SSO.Model.Aaron.Emun.LogType.LGSSERROR, UMUser.Uid, "處理子站登出請求:" + Err); isOk = false; } finally { request = DataConversion.getMORequest(SSOCookie.GetCookie("MO_request")); SSOCookie.DelCookie("MO_request"); SSOCookie.DelCookie("AppCode"); SSOCookie.DelCookie(SSOCookie.GetCookie("Token").ToString()); SSOCookie.DelCookie("Token"); postProcess.LogoutPost(request); if (SSOCookie.IsExist("Token") && SSOCookie.GetCookie("Token").ToString() == request.Token) { //SSOCookie.SetTokenExpiredTime(SSOCookie.GetCookie("Token").ToString(), DateTime.Now); Authentication.DelUserSite(); //SSOCookie.DelCookie("Token"); } if (isOk != true) { UMSvc.Logs.Debug(Fdays.SSO.Model.Aaron.Emun.LogType.LGSSERROR, UMUser.Uid, "處理子站登出請求失敗:" + Err); Jscript.AlertAndGoBack("操作失敗!" + Err); } } } #endregion

優點:實現比較簡單,容易擴展。
缺點:安全性不高,不支持跨域訪問;
上述代碼只是部份代碼,一些知識也並不是很全,如有錯誤也希望各位指出!大家共同學習學習。
如果有需要交流或者需要整份實現代碼與數據庫的請與我聯系或留下郵箱地址!