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
优点:实现比较简单,容易扩展。
缺点:安全性不高,不支持跨域访问;
上述代码只是部份代码,一些知识也并不是很全,如有错误也希望各位指出!大家共同学习学习。
如果有需要交流或者需要整份实现代码与数据库的请与我联系或留下邮箱地址!