zoukankan      html  css  js  c++  java
  • ASP.NET MVC SSO单点登录设计与实现

    实验环境配置

    HOST文件配置如下:

    127.0.0.1 app.com
    127.0.0.1 sso.com

    IIS配置如下:

    应用程序池采用.Net Framework 4.0

    注意IIS绑定的域名,两个完全不同域的域名。

    app.com网站配置如下:

     

     sso.com网站配置如下:

    memcached缓存:

     数据库配置:

     数据库采用EntityFramework 6.0.0,首次运行会自动创建相应的数据库和表结构。

    授权验证过程演示:

    在浏览器地址栏中访问:http://app.com,如果用户还未登陆则网站会自动重定向至:http://sso.com/passport,同时通过QueryString传参数的方式将对应的AppKey应用标识传递过来,运行截图如下:

    URL地址:http://sso.com/passport?appkey=670b14728ad9902aecba32e22fa4f6bd&username=

     输入正确的登陆账号和密码后,点击登陆按钮系统自动301重定向至应用会掉首页,毁掉成功后如下所示:

     由于在不同的域下进行SSO授权登陆,所以采用QueryString方式返回授权标识。同域网站下可采用Cookie方式。由于301重定向请 求是由浏览器发送的,所以在如果授权标识放入Handers中的话,浏览器重定向的时候会丢失。重定向成功后,程序自动将授权标识写入到Cookie中, 点击其他页面地址时,URL地址栏中将不再会看到授权标示信息。Cookie设置如下:

    登陆成功后的后续授权验证(访问其他需要授权访问的页面):

    校验地址:http://sso.com/api/passport?sessionkey=xxxxxx&remark=xxxxxx

    返回结果:true,false

    客户端可以根据实际业务情况,选择提示用户授权已丢失,需要重新获得授权。默认自动重定向至SSO登陆页面,即:http://sso.com /passport?appkey=670b14728ad9902aecba32e22fa4f6bd&username=seo@ljja.cn 同时登陆页面邮箱地址文本框会自定补全用户的登陆账号,用户只需输入登陆密码即可,授权成功后会话有效期自动延长一年时间。

    SSO数据库验证日志:

    用户授权验证日志:

    用户授权会话Session:

    数据库用户账号和应用信息:

    应用授权登陆验证页面核心代码:

    复制代码
      1 /// <summary>
      2     ///  公钥:AppKey
      3     ///  私钥:AppSecret
      4     ///  会话:SessionKey
      5     /// </summary>
      6     public class PassportController : Controller
      7     {
      8         private readonly IAppInfoService _appInfoService = new AppInfoService();
      9         private readonly IAppUserService _appUserService = new AppUserService();
     10         private readonly IUserAuthSessionService _authSessionService = new UserAuthSessionService();
     11         private readonly IUserAuthOperateService _userAuthOperateService = new UserAuthOperateService();
     12 
     13         private const string AppInfo = "AppInfo";
     14         private const string SessionKey = "SessionKey";
     15         private const string SessionUserName = "SessionUserName";
     16 
     17         //默认登录界面
     18         public ActionResult Index(string appKey = "", string username = "")
     19         {
     20             TempData[AppInfo] = _appInfoService.Get(appKey);
     21 
     22             var viewModel = new PassportLoginRequest
     23             {
     24                 AppKey = appKey,
     25                 UserName = username
     26             };
     27 
     28             return View(viewModel);
     29         }
     30 
     31         //授权登录
     32         [HttpPost]
     33         public ActionResult Index(PassportLoginRequest model)
     34         {
     35             //获取应用信息
     36             var appInfo = _appInfoService.Get(model.AppKey);
     37             if (appInfo == null)
     38             {
     39                 //应用不存在
     40                 return View(model);
     41             }
     42 
     43             TempData[AppInfo] = appInfo;
     44 
     45             if (ModelState.IsValid == false)
     46             {
     47                 //实体验证失败
     48                 return View(model);
     49             }
     50 
     51             //过滤字段无效字符
     52             model.Trim();
     53 
     54             //获取用户信息
     55             var userInfo = _appUserService.Get(model.UserName);
     56             if (userInfo == null)
     57             {
     58                 //用户不存在
     59                 return View(model);
     60             }
     61 
     62             if (userInfo.UserPwd != model.Password.ToMd5())
     63             {
     64                 //密码不正确
     65                 return View(model);
     66             }
     67 
     68             //获取当前未到期的Session
     69             var currentSession = _authSessionService.ExistsByValid(appInfo.AppKey, userInfo.UserName);
     70             if (currentSession == null)
     71             {
     72                 //构建Session
     73                 currentSession = new UserAuthSession
     74                 {
     75                     AppKey = appInfo.AppKey,
     76                     CreateTime = DateTime.Now,
     77                     InvalidTime = DateTime.Now.AddYears(1),
     78                     IpAddress = Request.UserHostAddress,
     79                     SessionKey = Guid.NewGuid().ToString().ToMd5(),
     80                     UserName = userInfo.UserName
     81                 };
     82 
     83                 //创建Session
     84                 _authSessionService.Create(currentSession);
     85             }
     86             else
     87             {
     88                 //延长有效期,默认一年
     89                 _authSessionService.ExtendValid(currentSession.SessionKey);
     90             }
     91 
     92             //记录用户授权日志
     93             _userAuthOperateService.Create(new UserAuthOperate
     94             {
     95                 CreateTime = DateTime.Now,
     96                 IpAddress = Request.UserHostAddress,
     97                 Remark = string.Format("{0} 登录 {1} 授权成功", currentSession.UserName, appInfo.Title),
     98                 SessionKey = currentSession.SessionKey
     99             }); 104 
    105             var redirectUrl = string.Format("{0}?SessionKey={1}&SessionUserName={2}",
    106                 appInfo.ReturnUrl, 
    107                 currentSession.SessionKey, 
    108                 userInfo.UserName);
    109 
    110             //跳转默认回调页面
    111             return Redirect(redirectUrl);
    112         }
    113     }
    复制代码

    Memcached会话标识验证核心代码:

    复制代码
    public class PassportController : ApiController
        {
            private readonly IUserAuthSessionService _authSessionService = new UserAuthSessionService();
            private readonly IUserAuthOperateService _userAuthOperateService = new UserAuthOperateService();
    
            public bool Get(string sessionKey = "", string remark = "")
            {
                if (_authSessionService.GetCache(sessionKey))
                {
                    _userAuthOperateService.Create(new UserAuthOperate
                    {
                        CreateTime = DateTime.Now,
                        IpAddress = Request.RequestUri.Host,
                        Remark = string.Format("验证成功-{0}", remark),
                        SessionKey = sessionKey
                    });
    
                    return true;
                }
    
                _userAuthOperateService.Create(new UserAuthOperate
                {
                    CreateTime = DateTime.Now,
                    IpAddress = Request.RequestUri.Host,
                    Remark = string.Format("验证失败-{0}", remark),
                    SessionKey = sessionKey
                });
    
                return false;
            }
        }
    复制代码

    Client授权验证Filters Attribute

    复制代码
    public class SSOAuthAttribute : ActionFilterAttribute
        {
            public const string SessionKey = "SessionKey";
            public const string SessionUserName = "SessionUserName";
    
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                var cookieSessionkey = "";
                var cookieSessionUserName = "";
    
                //SessionKey by QueryString
                if (filterContext.HttpContext.Request.QueryString[SessionKey] != null)
                {
                    cookieSessionkey = filterContext.HttpContext.Request.QueryString[SessionKey];
                    filterContext.HttpContext.Response.Cookies.Add(new HttpCookie(SessionKey, cookieSessionkey));
                }
    
                //SessionUserName by QueryString
                if (filterContext.HttpContext.Request.QueryString[SessionUserName] != null)
                {
                    cookieSessionUserName = filterContext.HttpContext.Request.QueryString[SessionUserName];
                    filterContext.HttpContext.Response.Cookies.Add(new HttpCookie(SessionUserName, cookieSessionUserName));
                }
    
                //从Cookie读取SessionKey
                if (filterContext.HttpContext.Request.Cookies[SessionKey] != null)
                {
                    cookieSessionkey = filterContext.HttpContext.Request.Cookies[SessionKey].Value;
                }
    
                //从Cookie读取SessionUserName
                if (filterContext.HttpContext.Request.Cookies[SessionUserName] != null)
                {
                    cookieSessionUserName = filterContext.HttpContext.Request.Cookies[SessionUserName].Value;
                }
    
                if (string.IsNullOrEmpty(cookieSessionkey) || string.IsNullOrEmpty(cookieSessionUserName))
                {
                    //直接登录
                    filterContext.Result = SsoLoginResult(cookieSessionUserName);
                }
                else
                {
                    //验证
                    if (CheckLogin(cookieSessionkey, filterContext.HttpContext.Request.RawUrl) == false)
                    {
                        //会话丢失,跳转到登录页面
                        filterContext.Result = SsoLoginResult(cookieSessionUserName);
                    }
                }
    
                base.OnActionExecuting(filterContext);
            }
    
            public static bool CheckLogin(string sessionKey, string remark = "")
            {
                var httpClient = new HttpClient
                {
                    BaseAddress = new Uri(ConfigurationManager.AppSettings["SSOPassport"])
                };
    
                var requestUri = string.Format("api/Passport?sessionKey={0}&remark={1}", sessionKey, remark);
    
                try
                {
                    var resp = httpClient.GetAsync(requestUri).Result;
    
                    resp.EnsureSuccessStatusCode();
    
                    return resp.Content.ReadAsAsync<bool>().Result;
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
    
            private static ActionResult SsoLoginResult(string username)
            {
                return new RedirectResult(string.Format("{0}/passport?appkey={1}&username={2}",
                        ConfigurationManager.AppSettings["SSOPassport"],
                        ConfigurationManager.AppSettings["SSOAppKey"],
                        username));
            }
        }
    复制代码

    示例SSO验证特性使用方法:

    复制代码
    [SSOAuth]
        public class HomeController : Controller
        {
            public ActionResult Index()
            {
                return View();
            }
    
            public ActionResult About()
            {
                ViewBag.Message = "Your application description page.";
    
                return View();
            }
    
            public ActionResult Contact()
            {
                ViewBag.Message = "Your contact page.";
    
                return View();
            }
        }
    复制代码

    总结:

    从草稿示例代码中可以看到代码性能上还有很多优化的地方,还有SSO应用授权登陆页面的用户账号不存在、密码错误等一系列的提示信息等。在业务代码 运行基本正确的后期,可以考虑往更多的安全性层面优化,比如启用AppSecret私钥签名验证,IP范围验证,固定会话请求攻击、SSO授权登陆界面的 验证码、会话缓存自动重建、SSo服务器、缓存的水平扩展等。

    源码地址:https://github.com/smartbooks/SmartSSO

  • 相关阅读:
    数据仓库-(3)企业级数仓实战分享课程-1.课程简介/2.数仓简介/3.基础概念/4.大数据技术栈与组件
    数据仓库-(2)企业级数仓介绍
    数据仓库-(1)数仓主题分享记录
    Spark学习小记-(1)DataFrame的schema
    Hive学习小记-(11)left semi join
    Hive学习小记-(10)hive增量下发的变化流水表如何做update操作
    Hive学习小记-(9)hive分区表加字段**
    Hive学习小记-(8)hive查询除某列外所有数据(正则表达式查询)
    Hive学习小记-(7)group by原理&tips
    我曾七次鄙视自己的灵魂
  • 原文地址:https://www.cnblogs.com/smallfa/p/5482317.html
Copyright © 2011-2022 走看看