十六、【适合中小企业的.Net轻量级开源框架】EnterpriseFrameWork框架核心类库之单点登录SSO
回《【开源】EnterpriseFrameWork框架系列文章索引》
EFW框架源代码下载:http://pan.baidu.com/s/1qWJjo3U
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
如上图,当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候,就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。
上面这段文字描叙是从网上摘抄的,觉得基本上把单点登录的原理讲得很清晰了;本章讲解EFW框架中是如何实现单点登录的以及框架中是如何使用的;
本文要点:
1.什么情况下会用到单点登录
2.框架中的三种模式Web、Winform和WCF,分别是怎样进行用户验证
3.单点登录样例
4.单点登录SSO实现代码
1.什么情况下会用到单点登录
刚开始框架中也是没有单点登录此模块的,有一次需要在Winform系统中嵌入Web页面,整合两个系统。Web页面的用户信息验证一直没找到什么好的解决办法,刚开始的办法是通过往网页地址后面自动加上登录用户名和密码,发送到后台进行登录;这样也达到了整合的目的,但是总感觉比较别扭,直接把用户名和密码暴露在地址栏肯定存在安全隐患。后来经过一番波折在网上找到类似的解决办法,利用单点登录的方案达到了比较好的效果;
除了上面说的Winform系统中嵌入Web页面这种情况,还有经常在自己公司系统中整合一些合作伙伴的系统,如此打包销售更有市场竞争力,这样首要解决的问题也是登录入口统一;如今行业软件现状,不像十年前了只有那么一两套系统,讲究着用就行了,现在没有用上十来个系统就不叫信息化了,所以你能提供一个单点登录的解决方案也是一大卖点;再就是现在的软件公司不管大小靠一个产品就能生存的很难了,基本都是最大化的挖掘客户的需求,最好能提供一整套的解决方案,这些系统能整体销售更好,而单个产品销售也得支持。所以不管是客户的需求还是内部的产品都会存在系统间整合的问题,而利用单点登录至少能解决用户统一验证的问题;
2.框架中的三种系统模式Web、Winform和WCF,分别是怎样进行用户验证
先分析一下框架中的三种系统模式的用户验证是如何实现的,然后才能正确运用单点登录的功能;
1)Web系统用户验证,输入用户名密码登录后,登录界面向后台发送登录请求调用LoggingController执行用户密码验证代码,验证正确后把用户信息存入Session;之后所有界面操作向后台发送请求,APIHttpHandler对象接收请求后,先判断Session中是否存在用户信息,只有存在才执行对应的控制器代码,否则再返回错误信息给前台;
这里增加了一个系统配置参数TurnOnLoginRight用来是否打开验证用户登录,这在我们开发系统中调试后台控制器很有用;
2)Winform系统用户验证,这个比较简单,用户登录后根据后台配置的用户权限,动态加载系统的菜单,所以后面的操作也不用再进行用户验证了;
3)WCF系统用户验证,就是上面两种的结合了,客户端如Winform系统,WCF中间件在WCFHandlerService服务中进行用户验证;
3.单点登录样例
用上面讲过的Winform系统嵌入Web页面这种情况下如何使用单点登录的功能;
在登录成功后调用框架中的SsoHelper对象的SignIn方法生成TokenKey;
然后把TokenKey值加入Web页面的URL地址之后,然后web页面向后台发送Ajax请求的时候把TokenKey当成参数传入后台,后台进行单点登录验证。
4.单点登录SSO实现代码
框架源代码目录结构:
其中外部调用SSO功能只需要调用SsoHelper对象就可以了,SsoHelper把SSO封装成外部调用的类;包括SignIn、SignOut、ValidateToken等方法;TokenManager类存储所有登录用户的信息,TokenInfo类封装的用户信息结构;
SsoHelper文件
1 /// <summary> 2 /// 单点登录辅助类 3 /// </summary> 4 public class SsoHelper 5 { 6 /// <summary> 7 /// 登录 8 /// </summary> 9 /// <param name="userId"></param> 10 /// <param name="tokenid"></param> 11 /// <returns></returns> 12 public static bool SignIn(string userId,string userName, out Guid tokenid) 13 { 14 TokenInfo existToken = TokenManager.GetToken(userId); 15 if (existToken != null) 16 { 17 tokenid = existToken.tokenId; 18 return true; 19 } 20 21 TokenInfo token = new TokenInfo() 22 { 23 tokenId = Guid.NewGuid(), 24 IsValid = true, 25 CreateTime = DateTime.Now, 26 ActivityTime=DateTime.Now, 27 UserId = userId, 28 UserName=userName//, 29 //RemoteIp = Utility.RemoteIp 30 }; 31 tokenid = token.tokenId; 32 return TokenManager.AddToken(token); 33 } 34 /// <summary> 35 /// 注销 36 /// </summary> 37 /// <param name="token"></param> 38 /// <returns></returns> 39 public static bool SignOut(Guid token) 40 { 41 return TokenManager.RemoveToken(token); 42 } 43 /// <summary> 44 /// 是否有效登录 45 /// </summary> 46 /// <param name="token"></param> 47 /// <returns></returns> 48 public static AuthResult ValidateToken(string token) 49 { 50 Guid guid= ConvertHelper.GetGuid(token, Guid.NewGuid()); 51 52 AuthResult result = new AuthResult() { ErrorMsg = "Token不存在" }; 53 TokenInfo existToken = TokenManager.GetToken(guid); 54 55 if (existToken != null) 56 { 57 #region 客户端IP不一致 58 //if (existToken.RemoteIp != entity.RemoteIp) 59 //{ 60 // result.ErrorMsg = "客户端IP不一致"; 61 //} 62 #endregion 63 64 if (existToken.IsValid == false) 65 { 66 result.ErrorMsg = "Token已过期" + existToken.ActivityTime.ToLongTimeString() + ":" + DateTime.Now.ToLocalTime(); 67 TokenManager.RemoveToken(existToken.tokenId);//移除 68 } 69 else 70 { 71 result.User = new UserInfo() { UserId = existToken.UserId,UserName=existToken.UserName, CreateDate = existToken.CreateTime }; 72 result.ErrorMsg = string.Empty; 73 } 74 } 75 76 return result; 77 } 78 79 /// <summary> 80 /// 定时触发登录码的活动时间,频率必须小于4分钟 81 /// </summary> 82 /// <param name="token"></param> 83 public static void UserActivity(Guid token) 84 { 85 TokenInfo existToken = TokenManager.GetToken(token); 86 existToken.ActivityTime = DateTime.Now; 87 } 88 89 /// <summary> 90 /// 用户是否在线 91 /// </summary> 92 /// <param name="userId"></param> 93 /// <returns></returns> 94 public static bool IsUserOnline(string userId) 95 { 96 TokenInfo existToken = TokenManager.GetToken(userId); 97 if (existToken != null) return true; 98 return false; 99 } 100 }
TokenManager文件
1 public class TokenManager 2 { 3 private const int _TimerPeriod = 60000;//60秒 4 private static Timer thTimer; 5 6 static List<TokenInfo> tokenList = null; 7 8 static TokenManager() 9 { 10 tokenList = new List<TokenInfo>(); 11 thTimer = new Timer(_ThreadTimerCallback, null, _TimerPeriod, _TimerPeriod); 12 } 13 14 public static bool AddToken(TokenInfo entity) 15 { 16 tokenList.Add(entity); 17 return true; 18 } 19 20 public static bool RemoveToken(Guid token) 21 { 22 TokenInfo existToken = tokenList.SingleOrDefault(t => t.tokenId ==token); 23 if (existToken != null) 24 { 25 tokenList.Remove(existToken); 26 return true; 27 } 28 29 return false; 30 } 31 32 public static TokenInfo GetToken(Guid token) 33 { 34 TokenInfo existToken = tokenList.SingleOrDefault(t => t.tokenId == token); 35 return existToken; 36 } 37 38 public static TokenInfo GetToken(string userId) 39 { 40 TokenInfo existToken = tokenList.SingleOrDefault(t => (t.UserId == userId && t.IsValid==true)); 41 return existToken; 42 } 43 44 private static void _ThreadTimerCallback(Object state) 45 { 46 DateTime now = DateTime.Now; 47 48 Monitor.Enter(tokenList); 49 try 50 { 51 // Searching for expired users 52 foreach (TokenInfo t in tokenList) 53 { 54 if (((TimeSpan)(now - t.ActivityTime)).TotalMilliseconds > _TimerPeriod) 55 { 56 t.IsValid = false;//失效 57 } 58 } 59 } 60 finally 61 { 62 Monitor.Exit(tokenList); 63 } 64 } 65 }
TokenInfo文件
1 public class TokenInfo 2 { 3 public Guid tokenId { get; set; } 4 5 public DateTime CreateTime { get; set; } 6 7 public DateTime ActivityTime { get; set; } 8 9 public string RemoteIp { get; set; } 10 11 public string UserId { get; set; } 12 13 public string UserName { get; set; } 14 15 public bool IsValid { get; set; } 16 }
参考资料:
编写你自己的单点登录(SSO)服务