65.9K
CodeProject 正在变化。 阅读更多。
Home

跨域 ASP.NET 应用程序的单点登录 (SSO):第二部分 - 实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (116投票s)

2010年9月30日

CPOL

24分钟阅读

viewsIcon

578505

downloadIcon

18218

一种用于 ASP.NET 应用程序的域无关单点登录 (SSO) 实现方法。

引言

在当今的万维网中,多个 Web 应用程序的单点登录是一个常见需求,当这些 Web 应用程序部署在不同域下时,实现起来并不容易。为什么?因为 Web 应用程序(尤其是在 ASP.NET 应用程序中)的用户身份验证和用户“登录”状态的维护完全依赖于 HTTP Cookie,如果两个 Web 应用程序部署在不同域下,它们就不能简单地共享一个 Cookie。

以下文章详细讨论了 ASP.NET 中的用户身份验证及其内部实现策略。它还深入分析了 ASP.NET 中一些单点登录实现方法及其优缺点。如果您尚未阅读,请查看此文章

实现

是的,我基于建议的模型完成了一个 SSO 示例应用程序。它不仅仅是另一个“Hello world”,它是一个跨三个不同域下三个不同站点的 SSO 工作应用程序。艰苦的工作已经完成,其输出是,您只需扩展一个类,即可在您的 ASP.NET 应用程序中使 ASPX 页面成为“单点登录”页面。当然,您必须设置一个 SSO 站点并配置您的客户端应用程序以使用 SSO 站点,仅此而已(只需 10 分钟的工作)。

SSO 实现基于以下高级架构

CrossDomainSSOExample/Proposed_SSO_model_overview.png

图:单点登录实现模型

可能有无限数量的客户端站点(在我们的示例中是 3 个站点)可以通过单个“单点登录”服务器(称之为 SSO 站点, www.sso.com)参与“单点登录”伞。如前一篇文章所述,在此模型中,浏览器不会为每个不同的客户端站点存储身份验证 Cookie。相反,它只会为 SSO 站点(www.sso.com)存储一个身份验证 Cookie,该 Cookie 将被其他站点用于实现单点登录。

在此模型中,对任何客户端站点(参与 SSO 模型)的每个请求都将内部重定向到 SSO 站点(www.sso.com),以设置和检查身份验证 Cookie 的存在。如果找到 Cookie,则为浏览器提供客户端站点(当前由浏览器请求)的已认证页面,如果未找到,则用户将被重定向到相应站点的登录页面。

工作原理

最初,浏览器没有为 www.sso.com 存储任何身份验证 Cookie。因此,在浏览器中访问 www.domain1.comwww.domain2.comwww.domain3.com 的任何受保护页面都会将用户重定向到登录页面(通过内部重定向到 www.sso.com 以检查身份验证 Cookie 的存在)。一旦用户登录到某个站点,浏览器中就会设置 www.sso.com 的身份验证 Cookie,其中包含登录用户信息(最重要的是用户令牌,它仅对用户登录会话有效)。

现在,如果用户访问 www.domain1.comwww.domain2.comwww.domain3.com 的任何受保护页面 URL,请求会在用户的浏览器中内部重定向到 www.sso.com,浏览器会发送已设置的身份验证 Cookie。www.sso.com 找到身份验证 Cookie,提取用户令牌,并使用用户令牌重定向到浏览器中最初请求的 URL,最初请求的站点会验证令牌并提供用户最初请求的页面。

一旦用户在此 SSO 模型下登录到任何站点,访问 www.domain1.comwww.domain2.comwww.domain3.com 上的任何受保护页面都会导致内部重定向到 www.sso.com(用于检查身份验证 Cookie 和检索用户令牌),然后将身份验证页面作为浏览器输出提供。

好的,给我看看你实现了什么

我开发了一个示例单点登录应用程序,该应用程序包含三个不同的站点(www.domain1.comwww.domain2.comwww.domain3.com)和一个 SSO 服务器站点(www.sso.com)。本篇文章提供了示例 SSO 实现代码供下载。您只需按照下一节中的说明下载并设置站点即可。完成此操作后,您可以在不同的场景中测试该实现。

以下部分提供了逐步说明,用于在不同场景中测试单点登录功能,每个测试场景都包含 Firebug 网络流量信息,其中描述了请求的总数(包括轻量级重定向请求)及其大小。重定向请求的数量及其大小用绿色标记,以便于理解。

场景1:认证前

在同一浏览器窗口的三个不同标签页中访问以下三个 URL。

  • www.domain1.com
  • www.domain2.com
  • www.domain3.com

CrossDomainSSOExample/LoginUrlSSO.png

每个不同的站点将在不同的标签页中显示三个不同的登录屏幕

CrossDomainSSOExample/Login1.png

CrossDomainSSOExample/Login2.png

CrossDomainSSOExample/Login3.png

流量信息

为了显示登录屏幕,总共向服务器发送了四个请求,其中三个是重定向请求(绿色标记)。重定向请求的大小非常小(以字节为单位),即使考虑网络延迟也可以忽略不计。

CrossDomainSSOExample/TrafficLogin.png

场景 2:身份验证

在任何一个登录屏幕中使用以下凭据之一登录。让我们使用 user1/123 登录 www.domain1.com

可用凭据

  • 用户1/123
  • 用户2/123
  • 用户3/123

登录后,将为 www.domain1.com 上的 user1 提供以下屏幕。

CrossDomainSSOExample/Home1.png

流量信息

登录时,总共向服务器发送了三个请求,其中两个是重定向请求(绿色标记)。重定向请求的大小非常小(以字节为单位),即使考虑到网络延迟也可以忽略不计。

CrossDomainSSOExample/TrafficAuthentication.png

场景 3:认证后

由于 user1 已登录 www.domain1.com,如果他在同一窗口或同一窗口的不同标签页中浏览其他剩余站点:www.domain2.comwww.domain3.com,他应该同时登录到这些站点。访问 www.domain2.comwww.domain3.com 中的受保护页面不应显示登录屏幕。

让我们刷新 www.domain2.comwww.domain3.com 在其相应窗口中的当前页面(目前,浏览器中显示的是登录屏幕)

CrossDomainSSOExample/Home2.png

CrossDomainSSOExample/Home3.png

您将看到,系统显示的不是登录屏幕,而是已认证的主页。因此,user1 已登录所有三个站点:www.domain2.comwww.domain2.comwww.domain3.com

每个主页都显示一个“转到个人资料页”链接,您可以点击该链接导航到另一个页面。这表明点击超链接和导航到应用程序中的其他页面也正常工作,没有任何问题。

流量信息

登录后浏览已认证页面时,总共向服务器发送了 3 个请求,其中 2 个是重定向请求(绿色标记)。重定向请求的大小也非常小(以字节为单位),即使考虑到网络延迟也可以忽略不计。

CrossDomainSSOExample/TrafficAuthentication.png

场景 4:在不同会话中访问已认证页面

如预期的那样,用户的“登录”状态应该仅对当前会话 ID 有效,并且对三个站点中任何一个站点的任何已认证页面 URL 的访问都将成功,如果该 URL 是在同一浏览器窗口或同一浏览器窗口的不同标签页中访问的。但是,如果打开一个新浏览器窗口,并在其中访问一个已认证的 URL,则不应该成功,并且请求应该被重定向到登录页面(因为那是一个不同的浏览器会话)。

为了测试这一点,打开一个新的浏览器窗口,并访问指向受保护页面的三个站点中的任何一个 URL(您可以复制并粘贴现有的 URL 地址)。这次,您将看到请求将被重定向到登录页面,而不是显示页面输出,如下所示(假设您访问了 www.domain3.com 的 URL)

CrossDomainSSOExample/Login3.png

流量信息

对于在不同会话中访问已认证页面,总共向服务器发送了 4 个请求,其中 3 个是重定向请求(绿色标记)。重定向请求的大小非常小(以字节为单位),即使考虑网络延迟也可以忽略不计。

CrossDomainSSOExample/TrafficLogin.png

退出

要从站点注销,请单击 www.domain1.com 主页上的“注销”链接。系统将从 www.domain1.com 站点注销 user1,并再次重定向到登录屏幕

CrossDomainSSOExample/Login1.png

流量信息

退出登录时,总共向服务器发送了 4 个请求,其中 3 个是重定向请求(绿色标记)。重定向请求的大小非常小(以字节为单位),即使考虑网络延迟也可以忽略不计。

CrossDomainSSOExample/TrafficLogout.png

退出后

由于 user1 已从 www.domain1.com 站点注销,他应该同时从 www.domain2.comwww.domain3.com 注销。因此,现在访问 www.domain2.comwww.domain3.com 的任何受保护页面 URL 都应该重定向到它们相应的登录屏幕。

为了测试这一点,请刷新 www.domain2.comwww.domain3.com 的当前页面。系统现在将重定向请求到它们的登录页面,而不是刷新页面。

CrossDomainSSOExample/Login2.png

CrossDomainSSOExample/Login3.png

流量信息

与登录相同。

太棒了……这似乎奏效了!我如何设置这些站点?

SSO 示例实现是使用 Visual Studio 2010、.NET 4.0 Framework 开发的,并在 Windows Vista 机器上的 IIS 7 中进行了测试。然而,它没有使用任何 4.0 Framework 特定的技术或类库,因此,如果需要,可以轻松地将其转换为使用较低级别的 Framework。

请按照以下步骤在您的机器上设置示例 SSO 实现

  • 下载 SSO.zip 并解压到您电脑中任何方便的位置。以下文件夹/文件将解压到“SSO”文件夹中
  • 点击“SSO.sln”在 Visual Studio 中打开解决方案,以了解解决方案结构中的“谁是谁”
  • CrossDomainSSOExample/SolutionExplorer.png

    顾名思义

    • C:\...\www.domain1.comwww.domain1.com 的网站
    • C:\...\www.domain2.comwww.domain2.com 的网站
    • C:\...\www.domain3.comwww.domain3.com 的网站
    • C:\...\www.sso.comwww.sso.com 的网站
    • SSOLib 是一个类库,代表客户端站点处理所有与单点登录相关的逻辑,并通过 Web 服务与 SSO 站点通信。
  • 在“运行”命令中键入“inetmgr”启动 IIS 管理器(或者,您可以从开始->程序文件导航到 IIS 管理器),并在其中创建一个名为“www.domain1.com”的站点
  • 右键单击“站点”并单击“添加网站...

    CrossDomainSSOExample/AddNewSite.png

    在以下输入表单中提供必要的输入,然后单击“确定”

    CrossDomainSSOExample/CreateSiteDomain1.png

    站点“www.domain1.com”将在 IIS 中创建。创建站点后,该站点在 IIS 资源管理器中可能显示红色叉号,表示该站点尚未启动(这在我的 Windows Vista 家庭高级版 IIS 中发生)。在这种情况下,您需要选择该站点并单击“重新启动”图标以确保它启动(“重新启动”图标位于 IIS 资源管理器屏幕的右中部分)。

    CrossDomainSSOExample/RestartSite.png

  • 按照之前的相同步骤,创建以下三个站点,指向其对应的正确物理文件夹位置
    • www.domain2.com
    • www.domain3.com
    • www.sso.com

    CrossDomainSSOExample/CreateAllSites.png

  • 点击 IIS 资源管理器中的“应用程序池”节点。应用程序池列表将显示在右侧窗格中,您将看到刚刚在 IIS 中创建的相应站点的应用程序池。
  • 确保所有应用程序池都在 .NET Framework 4.0 下运行(因为 Web 应用程序是使用 Framework 4.0 构建的)。为此,右键单击相应的应用程序池(与站点名称同名),并在表单中选择 .NET Framework 版本

    CrossDomainSSOExample/ApplicationPool.png

  • 编辑 Hosts 文件(C:\Windows\System32\drivers\etc\hosts),以便站点名称映射到 localhost 回环地址(127.0.0.1
  • 127.0.0.1       localhost
    127.0.0.1       www.domain1.com
    127.0.0.1       www.domain2.com
    127.0.0.1       www.domain3.com
    127.0.0.1       www.sso.com

如果一切都正确完成,您应该能够运行这些站点并按上述方式正确测试。否则,请从头开始检查步骤,以验证是否有任何遗漏或配置错误。

好的,我如何在我的站点中实现 SSO?

好问题。示例 SSO 实现运行良好。但是,作为开发人员,您可能更感兴趣如何使用已开发的功能在您的 ASP.NET 站点中实现 SSO。在实现 SSO 模型时,我尝试制作一个可插拔组件 (SSOLib.dll),以便它需要最少的编程更改和配置。假设您有一些现有的 ASP.NET 应用程序,您需要以下步骤来跨它们实现“单点登录”

  • 为每个 ASP.NET 应用程序添加对“SSOLib.dll”的引用,或添加对“SSOLib”项目的引用。
  • 设置 SSO 站点(参见前面的步骤)。
  • 配置您的 ASP.NET 应用程序以使用 SSO 站点。为此,只需在每个 ASP.NET 应用程序的 web.confing 中添加以下配置
  • <!--Configuration section for SSOLib-->
     <configSections>
        <sectionGroup name="applicationSettings" 
             type="System.Configuration.ApplicationSettingsGroup, System, 
                   Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
         <section name="SSOLib.Properties.Settings" 
            type="System.Configuration.ClientSettingsSection, System, 
                  Version=4.0.0.0, Culture=neutral, 
                  PublicKeyToken=b77a5c561934e089" 
            requirePermission="false" />
        </sectionGroup>
     </configSections>
     <applicationSettings>
        <SSOLib.Properties.Settings>
         <setting name="SSOLib_Service_AuthService" serializeAs="String">
           <value>http://www.sso.com/AuthService.asmx</value>
         </setting>
        </SSOLib.Properties.Settings>
     </applicationSettings>
     <appSettings>
           <add key="SSO_SITE_URL" 
             value="http://www.sso.com/Authenticate.aspx?ReturnUrl={0}" />
           <add key="LOGIN_URL" value="~/Login.aspx" />
           <add key="DEFAULT_URL" value="~/Page.aspx" />
       </appSettings>
    <!--End Configuration section for SSOLib-->

    注意:根据您的设置的 SSO 站点 URL 和您的应用程序特定需求修改配置值。

  • 修改登录页面和其他私有页面(仅对经过身份验证的用户可访问的页面)的代码隐藏类(*.aspx.cs),使其不再扩展 System.Web.UI.Page,而是扩展 SSOLib.PrivatePage 类。
  • 除了执行注销功能的现有代码外,调用 SSOLib.PrivatePage 基类中已提供给代码隐藏类的 Logout() 方法。此外,不要执行现有登录功能,而是调用 SSOLib.PrivatePageLogin() 方法,并执行其他现有的登录后代码(如果存在)。

就是这样!您应该已经完成 SSO 实施。

等等!我的页面已经扩展了一个基类

好的,您的 ASP.NET 应用程序中的代码隐藏类很可能已经扩展了一个基类。如果是这样,集成 SSOLib.PrivatePage 对您来说可能会更容易。

假设您的一个应用程序中已经有一个 BasePage 类,它被经过身份验证的页面(仅对经过身份验证的用户可访问的页面)的代码隐藏类扩展。在这种情况下,您可能只需要修改 BasePage,使其扩展 SSOLib.PrivatePage,而无需修改所有 ASPX 页面的代码隐藏类,这样就完成了。

class BasePage : SSOLib.PrivatePage
{
    ...
}

另一种选择是修改 SSOLib.PrivatePage 以扩展现有的 BasePage(您拥有源代码,您可以这样做),并按照建议修改所有已认证页面的现有 aspx.cs 类以扩展 SSOLib.PrivatePage。也就是说

class PrivatePage : BasePage 
{
    ...
}

如果现有 BasePage 类和 SSOLib.PrivatePage 类之间存在任何冲突的代码或方法,您可能需要修改这两个类中的一些代码。最好不要更改 SSOLib.PrivatePage 的代码,除非发现任何错误,并且最好根据需要更改现有 BasePage 的代码。但是,如果您确实需要更改 SSOLib.PrivatePage 的代码,请随时更改,这完全取决于您!

嗯...那么谁来处理所有的 SSO 事情呢?魔法在哪里?

好问题。在理想情况下,使用这个 SSO 示例模型,您无需编写任何面向登录的代码即可在您的 ASP.NET 应用程序中实现 SSO(除了一些配置和继承更改)。这怎么可能呢?谁在管理所有这些繁琐的 SSO 工作呢?

SSOLib 和 SSO 站点是两位魔术师,负责完成所有技巧。SSOLib 是一个 DLL,每个客户端站点都使用它来执行以下操作

  • 通过 Web 服务与 SSO 站点通信。
  • 重定向到 SSO 站点或登录页面,或提供请求的页面。

下图描绘了 SSOLib 在 SSO 模型中的作用

CrossDomainSSOExample/SSOLib.png

SSOLib 中最重要的是 PrivatePage 类,它由认证类的代码隐藏页面继承。该类继承 System.Web.UI.Page 类,并重写 OnLoad() 方法,如下所示

public class PrivatePage : Page
{
      protected override void OnLoad(EventArgs e)
      {
           //Set caching preferences
           SetCachingPreferences();
                      
           //Read QueryString parameter values
           LoadParameters();

           if (IsPostBack)
           {
               //If this is a postback, do not redirect to SSO site.
               //Rather, hit a web method to the SSO site 
               //to know user's logged in status
               //and proceed based on the status
               HandlePostbackRequest();
               base.OnLoad(e);
               return;
           }

           //If the current request is marked not 
           //to be redirected to SSO site, do not proceed
           if (SessionAPI.RequestRedirectFlag == false)
           {
               SessionAPI.ClearRedirectFlag();
               base.OnLoad(e);
               return;
           }

           if (string.IsNullOrEmpty(RequestId))
           {
               //Absence of Request Paramter "RequestId" means 
               //current request is not redirected from SSO site.
               //So, redirect to SSO site with ReturnUrl
               RedirectToSSOSite();
               return;
           }
           else
           {
               //Current request is redirected from 
               //the SSO site. So, check user status
               //And redirect to appropriate page
               ValidateUserStatusAndRedirect();
           }

           base.OnLoad(e);
       }
}

基本上,每当由于浏览器中访问 URL 而加载 Page 对象时,就会调用 OnLoad(),并且核心 SSO 逻辑在此方法中实现。所有代码都是自描述和文档化的,以描绘正在发生的事情。

更多关于 SSOLib 功能的信息,请参阅以下章节。

www.sso.com 做什么?

SSO 站点具有以下两个重要功能

  • 用户认证和用户检索 Web 服务,客户端站点通过 SSOLib DLL 访问这些服务,以认证用户并了解用户在 SSO 站点上的登录状态。以下服务可用
  • CrossDomainSSOExample/SSOWebServices.png

  • 使用 ASPX 页面 (Authenticate.aspx) 设置和检索用户认证 Cookie,SSOLib 将根据请求类型重定向到该页面以设置/检查或删除 Cookie。
  • 以下是 Authenticate.aspx 执行的核心功能。代码是自描述和文档化的,易于理解。

    protected void Page_Load(object sender, EventArgs e)
    {
       //Read request paramters and populate variables
       LoadRequestParams();
    
       if (Utility.StringEquals(Action, AppConstants.ParamValues.LOGOUT))
       {
           //A Request paramter value Logout indicates
           //this is a request to log out the current user
           LogoutUser();
           return;
       }
       else
       {
           if (Token != null)
           {
               //Token is present in URL request. That means, 
               //user is authenticated at the client site 
               //using the Login screen and it redirected
               //to the SSO site with the Token in the URL parameter, 
               //so set the Authentication Cookie
               SetAuthCookie();
           }
           else
           {
               //User Token is not available in URL. So, check 
               //whether the authentication Cookie is available in the Request
               HttpCookie AuthCookie = 
                  Request.Cookies[AppConstants.Cookie.AUTH_COOKIE];
               if (AuthCookie != null)
               {
                   //Authentication Cookie is available 
                   //in Request. So, check whether it is expired or not.
                   //and redirect to appropriate location based upon the cookie status
                   CheckCookie(AuthCookie, ReturnUrl);
               }
               else
               {
                   //Authentication Cookie is not available 
                   //in the Request. That means, user is logged out of the system
                   //So, mark user as being logged out
                   MarkUserLoggedOut();
               }
           }
       }
    }

看起来很不错。您是否必须处理任何关键问题?

又一个好问题。核心 SSO 逻辑似乎非常简单。也就是说

If current request is a PostBack, 
    If this is a PostBack in Login page (For Login)
        Do Nothing
    Else
        Do not redirect to SSO site. Rather, invoke a web service at SSO site 
        to know user's logged in status, using the User *Token. 
    If user is not logged out 
        Proceed the normal PostBack operation.
    Else 
        Redirect to login page
Else
    If current request is not redirected from the SSO Site, 
        Redirect it to SSO site with   setting ReturnUrl with 
        the current Request URL and parameters.
    Else
        Get user's Logged in status on SSO Site 
        by invoking a web service with user *Token

        If user is logged out there, 
            Redirect to Login page
        If current request is a page refresh, 
            Redirect to SSO site with ReturnUrl
        Else 
            Redirect to the originally requested URL
    End If
End If

*用户令牌是 GUID 的哈希码,它唯一标识用户在 SSO 站点上的登录。每次用户登录 SSO 站点时,都会在 SSO 站点生成令牌,此令牌 稍后用于设置身份验证 Cookie 和通过客户端站点检索用户对象。

但是,在实现 SSO 逻辑时,需要处理一些明显的问题。这些问题在上述逻辑中以粗体标记

实现“重定向到登录页面”和“重定向到最初请求的 URL

SSOLib.PrivatePage 根据情况重定向到 SSO 站点,或重定向到客户端站点当前请求的页面。但是,如果 SSOLib.PrivatePage 重定向到当前站点的一个页面,就会出现问题。由于每个受保护页面都扩展了 SSOLib.PrivatePage 类,因此从 SSOLib.PrivatePage 重定向到当前站点的一个页面将导致它一遍又一遍地重定向到自身,从而导致无限重定向循环。

为了解决这个问题,一个简单的修复方法是添加一个请求参数(例如,Redirect=false),以指示请求不应再重定向。但是,这将允许用户看到 Request 参数,并允许用户通过更改其值来“破解”系统。因此,我没有使用 Request 参数,而是在从 SSOLib.PrivatePage 重定向到当前站点的任何 URL 之前,使用了一个 Session 变量来停止进一步重定向。在 OnLoad() 中,我检查 Session 变量并重置它并返回,如下所示

//If the current request is marked not to be redirected to SSO site, do not proceed
if (SessionAPI.RequestRedirectFlag == false)
{
   SessionAPI.ClearRedirectFlag();
   return;
}

检测“当前请求不是从 SSO 站点重定向的”以及“当前请求是页面刷新

SSOLib.PrivatePage 重定向到 SSO 站点以设置或检查身份验证 cookie。SSO 站点完成其工作后,它使用 ReturnUrl 中设置的 URL 重定向回调用站点。

这也创建了一种场景,即客户端站点可能会再次重定向到 SSO 站点,而 SSO 站点又会再次重定向到客户端站点,从而创建无限重定向循环。与前一种情况不同,这次不能使用 Session 变量,因为重定向发生在 SSO 站点,并且客户端站点和 SSO 站点具有不同的 Session 状态。因此,应该使用 Request 参数值来防止一旦 SSO 站点重定向到客户端站点后进一步重定向到 SSO 站点。

但是,再次使用 Request 参数来阻止重定向会允许用户更改它并破坏正常功能。为了解决这个问题,Request 参数值使用 GUID 的哈希值设置(RequestId=Hash(New GUID)),并在 SSO 站点重定向回客户端站点 URL 之前将其附加。

重定向请求再次执行 SSOLib.PrivatePageOnLoad() 方法,这次它会找到 RequestId,这表明此请求是从 SSO 站点重定向回来的,因此不应再重定向到 SSO 站点。

但是,如果用户更改了查询字符串中的 RequestId 值并访问了 URL,或者用户只是刷新了当前页面呢?

由于每个不同的请求都将重定向到 SSO 站点(除了回发命中),在这种情况下,此请求应该像往常一样重定向到 SSO 站点。但是,请求 URL 已经包含 RequestId,尽管如此,请求仍应重定向到 SSO 站点。那么,SSOLib.PrivatePage 应该如何理解这一点呢?

只有一种方法。一个特定的 RequestId 应该只对 SSO 站点的每个特定重定向有效,一旦客户端站点从 Request 参数收到 RequestId,它应该立即过期,这样即使下一个 URL 命中包含相同的 RequstId,或者下一个 URL 包含无效值,它也会重定向到 SSO 站点。

已使用以下逻辑来处理此场景

if (string.IsNullOrEmpty(RequestId))
{
   //Absence of Request Paramter RequestId means current 
   //request is not redirected from SSO site.
   //So, redirect to SSO site with ReturnUrl
   RedirectToSSOSite();
   return;
}
else
{
   //Current request is redirected from the SSO site. So, check user status
   //And redirect to appropriate page
   ValidateUserStatusAndRedirect();
}

//And,

UserStatus userStatus = AuthUtil.Instance.GetUserStauts(Token, RequestId);
if (!userStatus.UserLoggedIn)
{
   //User is not logged in at SSO site. So, return the Login page to user
   RedirectToLoginPage();
   return;
}
if (!userStatus.RequestIdValid)
{
   //Current RequestId is not valid. That means, 
   //this is a page refresh and hence, redirect to SSO site
   RedirectToSSOSite();
   return;
}
if (CurrentUser == null || CurrentUser.Token != Token)
{
   //Retrieve the user if the user is not found 
   //in session, or, the current user in session
   //is not the one who is currently logged onto the SSO site
   CurrentUser = AuthUtil.Instance.GetUserByToken(Token);
   if (CurrentUser.Token != Token || CurrentUser == null)
   {
       RedirectToSSOSite();
       return;
   }
}

另一方面,在重定向到客户端站点之前,SSO 站点会生成一个 RequestId,将其附加到查询字符串,并将其作为键和值放入 Application 中。以下是 SSO 站点如何重定向回客户端站点的方式

/// <summary>
/// Append a request ID to the URl and redirect
/// </summary>
/// <param name="Url"></param>
private void Redirect(string Url)
{
    //Generate a new RequestId and append to the Response URL.
    //This is requred so that, the client site can always
    //determine whether the RequestId is originated from the SSO site or not
    string RequestId = Utility.GetGuidHash();
    string redirectUrl = Utility.GetAppendedQueryString(Url, 
              AppConstants.UrlParams.REQUEST_ID, RequestId);
    
    //Save the RequestId in the Application
    Application[RequestId] = RequestId;

    Response.Redirect(redirectUrl);
}

请注意,在重定向之前,RequestId 存储在 Application 作用域中,以标记此 RequestId 对此特定响应到客户端站点有效。一旦客户端站点收到重定向请求,它会执行 GetUserStatus() Web Service 方法,以下是 GetUserStatus() Web 方法如何从 Application 作用域中清除 RequestId,以便任何后续具有相同 RequestId 的请求或任何具有无效 RequestId 的请求都可以被追踪为无效 RequestId

/// <summary>
/// Determines whether the current request is valid or not
/// </summary>
/// <param name="RedirectId"></param>
/// <returns></returns>
[WebMethod]
public UserStatus GetUserStauts(string Token, string RequestId)
{
      UserStatus userStatus = new UserStatus();

      if (!string.IsNullOrEmpty(RequestId))
      {
          if ((string)Application[RequestId] == RequestId)

          {
              Application[RequestId] = null;
              userStatus.RequestIdValid = true;
          }
      }

      userStatus.UserLoggedIn = 
        HttpContext.Current.Application[Token] == null ? false : true;

      return userStatus;
}

获取用户在 SSO 站点上的登录状态

GetUserStatus() Web 服务方法在 UserStatus 对象中返回用户状态,该对象有两个属性:UserLoggedInRequestIdValid

一旦用户通过 Authenticate Web 服务方法登录 SSO 站点,它就会生成一个用户令牌(新 GUID 的哈希码),并使用该令牌作为键将用户令牌 存储在 Application 变量中。

/// <summary>
/// Authenticates user by UserName and Password
/// </summary>
/// <param name="UserName"></param>
/// <param name="Password"></param>
/// <returns></returns>
[WebMethod]
public WebUser Authenticate(string UserName, string Password)
{
   WebUser user = UserManager.AuthenticateUser(UserName, Password);
   if (user != null)
   {
       //Store the user object in the Application scope, 
       //to mark the user as logged onto the SSO site
       //Along with the cookie, this is a supportive way 
       //to trak user's logged in status
       //In order to track a user as logged onto the SSO site 
       //user token has to be presented in the cookie as well as
       //he/she has to be presented in teh Application scope
       HttpContext.Current.Application[user.Token] = user;
   }
   return user;
}

当用户从任何客户端站点退出系统时,身份验证 cookie 将被移除,并且用户对象也会从 Application 作用域中移除(在 SSO 站点的 Authenticate.aspx.cs 中)。

/// <summary>
/// Logs out current user;
/// </summary>
private void LogoutUser()
{
   //This is a logout request. So, remove the authentication Cookie from the response
   if (Token != null)
   {
       HttpCookie Cookie = Request.Cookies[AppConstants.Cookie.AUTH_COOKIE];

       if (Cookie.Value == Token)
       {
           RemoveCookie(Cookie);
       }
   }
   //Also, mark the user at the application scope as null
   Application[Token] = null;

   //Redirect user to the desired location
   //ReturnUrl = GetAppendedQueryString(ReturnUrl, 
   //   AppConstants.UrlParams.ACTION, AppConstants.ParamValues.LOGOUT);
   Redirect(ReturnUrl);
}

因此,无需重定向到 SSO 站点,只需检查 SSO 站点的 Application 作用域中是否存在用户,即可得知用户的登录状态。客户端站点调用 SSO 站点的 Web 服务方法,SSO 站点在 UserStatus 对象中返回用户的登录状态。

这种了解用户登录状态的方法很方便,因为当发生回发时,客户端站点不希望重定向到 SSO 站点(因为如果这样做,回发事件方法将无法执行)。

在这种情况下,它们会调用 Web 方法来了解用户的登录状态,如果 SSO 站点上没有该用户,则当前请求会被重定向到登录页面。否则,会执行正常的 Postback 事件方法。

等一下,将用户存储在 Application 作用域中应该会标记用户在所有会话中都已登录。您如何处理这个问题?

没错。一旦用户通过身份验证,他/她就会被存储在 Application 作用域中,以标记为已登录。但是,Application 作用域是一个全局作用域,与站点和用户会话无关。因此,存在用户也可能在所有浏览器会话中被标记为已登录的风险。

这听起来很冒险。但是,我们小心地处理了这个问题,以确保特定浏览器会话的用户对象不会被其他浏览器会话访问。现在让我们看看是如何处理的。

一旦用户登录 SSO 站点,该用户就会以用户 Token 作为键存储在 Application 作用域中,该 Token 仅对特定用户登录会话有效。

如果通过从地址栏复制 URL,在新窗口(因此带有新 Session)中直接访问带有用户令牌(无论是否带有 RequestId)的请求,系统将不允许该 URL 请求绕过登录屏幕。为什么?因为 SSO 站点设置的身份验证 Cookie 是“非持久性”Cookie,因此只有当在同一浏览器会话中(从同一浏览器窗口或同一窗口的不同标签页)发起后续请求时,浏览器才会将此 Cookie 发送到 SSO 站点。这意味着,如果打开一个新的浏览器窗口,它没有要发送到 SSO 站点的任何身份验证 Cookie,自然,请求会被重定向到客户端站点的登录页面。因此,即使某个用户存储在 SSO 站点的 Application 作用域中,该用户对象也是以不同的用户令牌作为键存储的,该令牌永远无法在新会话中的任何新请求中访问,因为此请求不知道现有的用户令牌,一旦用户登录到此新浏览器会话,它就会获得一个新的用户令牌,该令牌永远不会与现有令牌匹配。

如何维护 Cookie 超时和滑动过期?

SSO 站点的 web.config 具有配置 Cookie 超时值以及启用/禁用 Cookie 滑动过期的配置选项。

<appSettings>
    <add key="AUTH_COOKIE_TIMEOUT_IN_MINUTES" value="30"/>
    <add key="SLIDING_EXPIRATION" value="true"/>
</appSettings>

Cookie 超时值可以在 SSO 站点的 web.config 中配置,并且超时值适用于 SSO 下的所有客户端站点。也就是说,如果 web.config 中指定的 Cookie 超时值为 30 分钟,并且 user1 登录到 www.domain1.com,则 Cookie 在浏览器中可用 30 分钟,因此 user1 在此 30 分钟内登录到其他两个站点,除非 user1 从站点注销。

那么,这个 Cookie 超时是如何实现的呢?很简单,当然是通过设置 Cookie 过期时间。

不幸的是,我没能做到。为什么?因为默认情况下,当在 Response 中设置 Cookie 时,它会作为非持久性 Cookie 创建(Cookie 只存储在浏览器内存中,用于当前会话,而不是客户端磁盘)。如果为 Cookie 指定了过期日期,ASP.NET 运行时会自动指示浏览器将 Cookie 存储为持久性 Cookie。

在我们的案例中,我们不想创建持久性 cookie,因为这会让其他会话也将身份验证 cookie 发送到 SSO 站点,并最终将用户标记为已登录。我们不希望发生这种情况。

但是,过期日期时间必须以某种方式设置。所以,我将过期值存储在 cookie 的值中,同时附加到用户的令牌中,如下所示

/// <summary>
/// Set authentication cookie in Response
/// </summary>
private void SetAuthCookie()
{
   HttpCookie AuthCookie = new HttpCookie(AppConstants.Cookie.AUTH_COOKIE);

   //Set the Cookie's value with Expiry time and Token
   int CookieTimeoutInMinutes = Config.AUTH_COOKIE_TIMEOUT_IN_MINUTES;

   AuthCookie.Value = Utility.BuildCookueValue(Token, CookieTimeoutInMinutes);
   //Appens the Token and expiration DateTime to build cookie value

   Response.Cookies.Add(AuthCookie);

   //Redirect to the original site request
   ReturnUrl = Utility.GetAppendedQueryString(ReturnUrl, 
                        AppConstants.UrlParams.TOKEN, Token);
   Redirect(ReturnUrl);
}

/// <summary>
/// Set cookie value using the token and the expiry date
/// </summary>
/// <param name="Value"></param>
/// <param name="Minutes"></param>
/// <returns></returns>
public static string BuildCookueValue(string Value, int Minutes)
{
    return string.Format("{0}|{1}", Value, 
       DateTime.Now.AddMinutes(Minutes).ToString());
}

最终,当 SSO 站点收到 Cookie 时,其值会按如下方式检索

/// <summary>
/// Reads cookie value from the cookie
/// </summary>
/// <param name="cookie"></param>
/// <returns></returns>
public static string GetCookieValue(HttpCookie Cookie)
{
   if (string.IsNullOrEmpty(Cookie.Value))
   {
       return Cookie.Value;
   }
   return Cookie.Value.Substring(0, Cookie.Value.IndexOf("|"));
}

并且,过期日期时间检索如下

/// <summary>
/// Get cookie expiry date that was set in the cookie value
/// </summary>
/// <param name="cookie"></param>
/// <returns></returns>
public static DateTime GetExpirationDate(HttpCookie Cookie)
{
   if (string.IsNullOrEmpty(Cookie.Value))
   {
       return DateTime.MinValue;
   }
   string strDateTime = 
     Cookie.Value.Substring(Cookie.Value.IndexOf("|") + 1);
   return Convert.ToDateTime(strDateTime);
}

如果在 web.config 中将 SLIDING_EXPIRATION 设置为 true,则每次请求都会增加 Cookie 过期日期时间值,增加的值是在 web.configAUTH_COOKIE_TIMEOUT_IN_MINUTES 中指定的分钟数。以下代码执行此操作

/// <summary>
/// Increases Cookie expiry time
/// </summary>
/// <param name="AuthCookie"></param>
/// <returns></returns>
private HttpCookie IncreaseCookieExpiryTime(HttpCookie AuthCookie)
{
   string Token = Utility.GetCookieValue(AuthCookie);
   DateTime Expirytime = Utility.GetExpirationDate(AuthCookie);
   DateTime IncreasedExpirytime = 
     Expirytime.AddMinutes(Config.AUTH_COOKIE_TIMEOUT_IN_MINUTES);

   Response.Cookies.Remove(AuthCookie.Name);

   HttpCookie NewCookie = new HttpCookie(AuthCookie.Name);
   NewCookie.Value = 
     Utility.BuildCookueValue(Token, Config.AUTH_COOKIE_TIMEOUT_IN_MINUTES);

   Response.Cookies.Add(NewCookie);

   return NewCookie;
}

这个实现可以用于生产系统吗?

是的!它当然可以使用,但在此之前,必须解决一些安全问题和其他横向问题。这只是一个基本实现,我没有通过专业的质量保证流程验证模型(尽管我自己做了一些基本的验收测试)。此外,这种身份验证不提供 Forms 身份验证提供的全部灵活性和功能。此外,它没有 Forms 身份验证的内置授权机制,因此您可能需要根据您的具体要求对当前的 SSO 实现进行一些额外的自定义。

但是,我将尝试更新 SSO 模型,以丰富其功能并使其更加健壮,以便可以在商业系统中使用而无需任何自定义。

任何建议或反馈都非常欢迎。再见!

© . All rights reserved.