单点登录实现思路

转载 2011年01月13日 09:04:00

我的想法是使用集中验证方式,多个站点集中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。

--------

 

相关文章推荐

深入学习微框架:Spring Boot

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过...

SSO单点登录三种情况的实现方式详解

单点登录(SSO——Single Sign On)对于我们来说已经不陌生了。对于大型系统来说使用单点登录可以减少用户很多的麻烦。就拿百度来说吧,百度下面有很多的子系统——百度经验、百度知道、百度文库等...

单点登录的实现思路

在项目开发的过程中,经常会出现这样的情况:我们的产品包括很多,如多个Web网站(前台、会员空间、管理平台等等)、C/S架构程序、客户关系系统等等,传统的身份验证方式已经大大的阻碍了我们各个产品之间的耦...

多个站点单点登录的设计思路

多个站点单点登录的设计思路 2010-07-28 当前各门户一般也都实现了多个业务之间的单点登录。下面根据我经历过的项目,谈一下我自己的看法。 一般来说单点认证都需要两端来完成,...

CSA实现SSO单点登录

  • 2011-06-22 10:08
  • 2.29MB
  • 下载

使用WIF实现单点登录Part I——Windows Identity Foundation介绍及环境搭建

上个月有一个星期的时间都在研究asp.net mvc统一身份验证及单点登录的实现。经过了一番的探索,最终决定使用微软的Windows Identity Foundation。但是这东西用的人貌似不多,...

单点登录SSO的实现原理

单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)