导出 (0) 打印
全部展开

利用 Azure AD 开发多租户 Web 应用程序

更新时间: 2014年5月

note备注
此示例已过时,其技术、方法和/或用户界面说明已由较新的功能替换。若要查看生成类似应用程序的已更新示例,请参阅 WebApp-MultiTenant-OpenIdConnect-DotNet

在面向开发人员的 Azure AD 系列的第一篇文章中,你已了解如何利用 Azure AD 租户让你的用户享受使用本地和云中的业务线 (LoB) 应用程序进行 Web 登录的体验。请在此处阅读该文章:使用 Azure AD 为 Web 应用程序添加登录

第二篇文章建立在第一篇文章的基础之上,说明了如何使用 LoB 应用通过 Graph API 查询目录信息。请在此处阅读该文章:使用 Graph API 查询 Azure AD

你现在正在阅读的文章会将你的 Azure AD 知识提升到一个新的水平,它演示了如何充分利用你在前两篇文章中了解的 Azure AD 功能,开发适合多个租户并可自动加入新客户的软件即服务 (SaaS) 应用程序。

就标识和访问而论,SaaS 应用开发人员必须面对的共同难题主要是关于客户的加入以及如何访问客户的身份基础结构。每个潜在客户都有一个不同的 Web 登录解决方案,因此很难标准化加入流程,使之既对于客户来说很简单又对于应用来说易于管理;每个潜在客户都在无法从云应用程序访问的基础结构中维护标识和目录数据。

Azure AD 为这两个难题提供了简单的解决方案。在第一篇文章中,你了解了管理员如何使用 Azure 管理门户注册你开发的应用程序,使之可以访问你的目录租户。在本演练中,你将了解到,还可以将同一应用程序配置为允许其他 Azure AD 租户访问。Azure AD 提供了一个机制,应用程序可自行通过该机制要求潜在客户的管理员向其目录租户授予访问权限。这通过使用 Azure AD 管理门户向客户的管理员呈现同意 UI 来实现:该体验与所有最常用社交 Web 应用程序中通常用来表示同意的手势相比并没有很大的不同。

本文档将显示如何修改你在前两篇文章中开发的 MVC 4 应用的 Azure AD 中的条目,使该应用可供多个 Azure AD 租户使用。此外,它还将指导你完成需要应用到应用的代码更改,以便从旨在连接到单个租户的解决方案转换为可以加入多个客户组织的完整多租户解决方案。演练说明将与高级概念的解释交织在一起,这些概念是你了解 Azure AD 在此处所述的示例应用程序的狭窄作用域外如何工作所必需的。

重申一遍:这里所述的任务都是在前两篇文章中开发的解决方案的基础上进行扩展的:你应该先阅读前两篇文章并按照其中的说明进行操作,然后才开始阅读本文。

本文档分为以下各节:

  • 先决条件:此节列出了完成本演练必须满足的所有要求。

  • 解决方案体系结构:此节概要介绍了对 SaaS 应用程序进行构造以利用 Azure AD 的一种方式。

  • 将 Windows AD 中的应用程序项提升为可在外部使用:此节将再次在 Azure 门户中访问 LoB 应用程序条目,并将显示如何更改才能将现有应用的可用性扩展到你自己的目录租户以外的目录租户。

  • 在 VS 中准备用于处理多个租户的应用程序项目:此节将显示如何通过修改 MVC 4 应用(该应用目前设计为使用单个目录租户执行 Web 登录和 Graph 访问)的源代码来处理一组动态的 Azure AD 租户。你将完成几个小节的内容,这几个小节将逐个解决在多租户情况下也具有普遍意义的各种应用标识管理方面的问题。

  • 将注册功能添加到应用程序:此节将显示如何通过修改 MVC 4 应用(该应用目前设计为使用单个目录租户执行 Web 登录和 Graph 访问)的源代码来处理一组动态的 Azure AD 租户。你将完成几个小节的内容,这几个小节将逐个解决在多租户情况下也具有普遍意义的各种应用标识管理方面的问题。

  • 可选:创建测试订阅:在第一个教程中创建的 Azure 订阅只包含一个目录(你自己的目录):但是,若要测试你的应用如何通过多租户工作负荷进行操作,你需要具有对自己租户以外的 Azure AD 租户的访问权限。此节将建议一个用于获取访问权限的策略。

  • 测试应用程序:此节将显示如何将本演练中执行的所有任务串联起来,建立连贯的 SaaS 应用程序端到端体验。

完成本教程需要以下先决条件:

多租户应用程序的解决方案体系结构

传统业务线应用程序适合由首先开发并部署该应用的组织中的用户进行访问和维护。 与此相反,以软件即服务 (SaaS) 形式提供的业务应用程序由独立软件供应商开发并运行,适合第三方组织使用。将以下应用程序构建为多租户资源是很常见的:由多个客户组织(租户)共享的应用程序,其中每个租户体验该应用时就好像自己是其唯一的客户。

如概述中所述,Azure AD 为你提供了一条路径,你可以通过该路径使用现有的 LoB 应用程序,并使该应用程序可供其他 Azure AD 租户管理员(你的潜在客户)在其组织中使用。所有潜在客户的管理员都只需导航到特别的 URL 并登录其 Azure AD 租户即可。他/她将登录到一个介绍该应用、该应用的发布者以及该应用需要的对你目录的访问级别(SSO、SSO 和只读访问权限、SSO 和读写访问权限)的页面上。客户的管理员可以使用页面上的控件同意应用程序访问其 Azure 目录,在这种情况下,管理门户将自动在客户的 Azure AD 租户中注册该应用,而不需要任何进一步的工作。

LoB 演练中创建的 MVC 4 应用程序包含对配置为与之集成的 Azure AD 租户的直接引用:也就是说,你已将该应用配置为将每个未经身份验证的请求重定向到租户特定的登录地址,并只接受来自该特定租户的令牌。你将在本演练中做的大多数工作均包括使应用的标识管理逻辑通用化,这样该应用就可以使用任何已注册的客户租户处理 Web 登录和 Graph API 调用。另一个重要的功能区域将添加将潜在客户定向到上述同意页的功能,并对结果进行处理,使已被接受为有效机构的 Azure AD 租户的列表动态增长。所有这些都将通过在少数策略区域中利用 WIF 的扩展性模型来完成。

已修改应用的体系结构的中心组件是 MultiTenantIssuerNameRegistry(从现在开始缩写为 MTINR)以及其租户的永久性存储。

MTINR 维护着应视为有效身份验证源的所有 Azure AD 租户的列表。该列表具有以下特征:

  • 在登录时查阅。只有注册租户颁发的令牌才视为有效

  • 在注册时更新。当潜在客户同意应用访问其目录时,管理门户会使用关联的租户 ID 将用户重定向回应用。本演练将显示如何以编程方式处理该消息以将新客户添加到 MTINR 列表。

MTINR 还保留着应该用于检查来自 Azure AD 的令牌上的签名的密钥;本文档将显示如何修改自动密钥刷新逻辑,以确保你的应用跟踪服务上的任何密钥滚动事件并最大限度地减少停机时间。

添加注册步骤意味着你的应用现在必须也能够向未经身份验证的用户提供 UI;你将了解如何更改 WIF 设置以便选择性地对控制器应用 Web 登录要求,而不是应用在 LoB 应用程序演练中演示的全面保护策略。

第一步,你将让 Azure AD 知道你要将应用程序设为可供其他 Azure AD 租户使用。为此,你将在 Azure 管理门户中更改应用程序设置。

导航到 Azure 管理门户,使用你用于完成 LoB 和 Graph API 演练的同一帐户登录。转到 Active Directory 选项卡,选择目录,单击“应用程序”标题,查找你在 LoB 教程中为应用创建的条目,然后单击该条目。

你将登录到注册过程结束时向你呈现的同一“快速启动”页;单击“配置”标题​​。

note备注
如果在上次演练期间你已选中“下次访问时跳过快速启动”框,你将直接到达“配置”屏幕。

外部访问权限

在浏览到页面一半的位置,你将发现标记为“外部访问”的开关。

此开关的默认位置是“关”:在创建时,应用程序已配置为只能由在其中创建它们的 Azure AD 租户中的用户访问。

打开外部访问意味着其他租户可以在自己的目录中设置该应用程序。具体而言,这意味着 Azure AD 将激活客户同意 URL,其他租户的管理员可以使用该 URL 授予应用程序访问管理员自己的目录的权限。当他们同意你的应用程序时,将触发在他们自己的 Azure AD 租户中设置一个描述该应用程序的条目的操作,同时将在他们的页面中设置应用参数(应用 ID URI应用 URL、用于访问 Graph API 的密钥等)。

Important重要提示
为应用程序启用外部访问这一行为不会更改应用程序已具有的针对你自己的 Azure AD 租户的任何现有权限。此外:针对应用程序禁用外部访问这一行为不会影响当应用程序可用时在你或他人的租户中授予应用程序的访问级别。换句话说,此开关只影响外部客户的同意体验和关联的设置机制的可用性;它并不影响它帮助创建的应用程序项。

单击此开关的“开”的一端。你会注意到,按钮将变为紫色,屏幕底部的命令栏现在提供“保存”按钮。尝试单击“保存”按钮:它将不起作用,但让自己熟悉生成的错误消息对你来说很重要。

更新配置失败

更新操作失败。单击“详细信息”图标以解决此问题。

失败详细信息

该消息指出,应用 ID URI 格式对此操作无效。

鉴于可从外部使用的应用程序将可以被更多受众访问(这些受众可能之前与提供应用的组织没有业务联系),Azure AD 将对这种应用强加一些额外要求,以便于识别这些受众。

虽然 LoB 应用可以将任何 URI 作为应用 ID URI 值,但在只将租户级唯一性作为唯一约束的情况下,应用程序需要符合以下要求才能被设置为可从外部使用:

  • 只接受使用 https:// 作为协议方案的 URI

  • URI 的主机部分必须与关联到你的 Azure AD 租户的已验证域相对应

如果你有一个自定义域要用于 Azure AD,则可以按照此处的说明验证它。你可以在与你租户的 Azure 管理门户页中的“应用程序”标题同级的“域”标题下找到已验证的自定义域。

如果你没有或不想使用自定义域,则可以利用已分配到每个 Azure AD 租户的 <tenantname>.onmicrosoft.com. 形式的默认三层域。鉴于它是一个安全的默认选项,我们在此处将遵循这样的策略。

向下滚动到页面底部的单一登录部分。通过输入值 https://<tenantname>.onmicrosoft.com/ExpenseReport,并将 <tenantname> 字符串替换为你的 Azure AD 租户的名称,来编辑应用 ID URI。再次单击“保存”

如果“应用 ID URI”格式是所需的格式,则这次更新操作将成功。管理门户将使用消息烤箱在屏幕底部通知你,但是,你也可以通过应用设置中的几个关键更改来判断,如下图所示。

用于授予访问权的 URL

外部访问开关现在已完全处于“开”的位置。此外,你还会注意到一个标记为“用于授予访问权限的 URL”的新文本框。那是潜在客户将用于向应用授予目录访问权限的终结点 URL;稍后在本演练中,当我们将注册功能整合到应用程序代码中时,我们将详细研究 URL 结构。

你的 Azure AD 租户关心的是,此时你的应用程序已准备好按多租户容量运行:现在,你需要相应地修改应用程序的代码。

note备注
本教程仍然在应用生命周期的开发阶段进行操作。你的应用为生产做好准备后,你会回到此屏幕来细化应用在同意页中的外观(通过上载适当的徽标),并将应用的答复 URL 更改为其生产地址。对于后者,你可以使用 LoB 应用演练的“在 Azure 网站中部署应用”一节中提供的说明:实际的部署步骤会因你的目标环境而异,但用于在管理门户中更新应用条目的说明可以按原样应用。

在 Visual Studio 2012 中打开你在前面的演练中创建的 MVC 4 项目。

正如在“解决方案体系结构”一节中所预期的,若要将多租户功能添加到你在前两个演练中创建的 LoB 应用程序,将需要应用几个有针对性的更改。 我们将根据此系列的演练的剩余内容,按功能区域以递增方式应用这些更改,主要目的是帮助你了解所发生的情况,使你可以将所学内容应用到自己的应用程序。

note备注
此处建议的更改也尝试最大程度地减少对目前所创建的代码的重构。这将导致一些冗余(例如,创建新的控制器而不是在一个更通用的控制器下整合多种功能),使本教程更易于遵循,不过你可以根据自己项目的需要,随意重构功能并使之标准化。

我们的第一项任务,就是更新 Web.config 中的 WS-Federation 对等项以反映新设置。在项目资源管理器中找到 Web.config 并打开它。

note备注
如果你尚未阅读 LoB 演练中的“详细的 WIF 设置”高级部分,现在最好阅读一下。其中一些任务可以使用标识和访问工具执行,但我们选择通过直接编辑 Web.config 来说明如何执行此操作,因为这样可以更清楚地说明各种必需的更改。

向下滚动到 <system.identityModel> 节。你将注意到 identityConfiguration/AudienceURI 元素仍包含旧的应用 ID URI 值:将它更改为你在 Azure 管理门户中输入的新值,如下所示。

<system.identityModel>
    <identityConfiguration>
      <audienceUris>
        <add value=”https://<tenantname>.onmicrosoft.com/ExpenseReport” />
      </audienceUris> 

这将确保该应用将在新应用 ID URI 的范围内接受相关令牌为有效令牌。

继续滚动到 system.identityModel.Services/federationConfiguration 中的 <wsFederation> 元素。

在这里你将需要应用两个更改:

  • realm 特性值修改为你对上面的 <audienceUris> 节使用的应用 ID URI。

  • 通过将 tenantID GUID 替换为字符串 common 来编辑 issuer 特性值。

<federationConfiguration>
   <cookieHandler requireSsl="false" />
   <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/common/wsfed" realm="https://<tenantname>.onmicrosoft.com/ExpenseReport" requireHttps="false" />
</federationConfiguration> 

realm 值对应于 WS-Federation 参数 wtrealm 的值,后者包含在向机构(标识提供者)发送的登录消息中以指示所请求令牌的预期接收者;它必须与用于标识目标 Azure AD 租户中的应用的值匹配。

issuer 值指示应接收登录请求的终结点。而在对应于你的 Azure 租户的第一个教程中,我们现在无法预先知道,下一个用户将来自哪个租户(在所有向应用授予许可的人员中)。Azure AD 提供一个特殊的终结点(通常称为与租户无关的终结点),它允许应用将应使用哪个租户的决定推迟到用户输入其用户名那一刻。鉴于用户名包含域信息,因此可以在该时刻具体确定应选择哪个租户,而身份验证也将如常进行。

note备注
请务必记住,仅当已在用户的 Azure AD 租户内向接收方应用程序(如前所述,由 realm 参数指示)授予访问权限时,Azure AD 才会为该用户颁发令牌。使用与租户无关的终结点不会减弱在目录级别建立的访问约束;它只是增加了登录过程的通用性。

再次在 Web.config 文件中:滚动到 IssuerNameRegistry 元素。

你应该记得,在第一个教程中,此元素记录用于定义有效令牌的参数:应该用于验证令牌签名的 X.509 证书的指纹,以及用于标识受信任的 Azure AD 租户的颁发者值。

该信息在多租户情况下仍然相关:主要区别是,该应用的客户租户存在一个对应的可接受颁发者的列表,而不是单个值。 用于实现 IssuerNameRegistryValidatingIssuerNameRegistry (VINR) 的类可以使用其 <validIssuers> 元素中的多个条目。但是,多租户应用程序在运行时及为现有客户提供服务时,可能会获得新客户,编辑 Web.config 可能会对应用的可用性造成意想不到的后果。

为了避免该问题,在本教程中,我们会将租户的信息存储到一个外部文件中,并将创建自定义 VINR 实现以将该外部文件用作验证对等项的源。

note备注
本教程通过实现几乎是最少的功能来演示存储的功能角色。在实际工作中,应用程序性能、可用性和存储的稳健性将是解决方案的主要方面。

让我们从创建外部存储开始。在“内容”文件夹下创建新的 XML 文件(在解决方案资源管理器中,右键单击“内容”“添加新项”,选择左侧的“数据”类别,选取 XML 文件)并称之为 tenants.xml

通过粘贴以下内容来编辑该文件:

<?xml version="1.0" encoding="utf-8" ?>
<authority>
  <tenants>
    <tenant id="95ca3807-2313-4cfe-93b3-20ef9f46ae88" />    
  </tenants>
  <keys>
    <key id="3A38FA984E8560F19AADC9F86FE9594BB6AD049B" />
  </keys>
</authority>

你可以将 <tenants><keys> 元素留空,也可以使用当前 IssuerNameRegistry 中的 thumbprinttenantID 值植入这两个元素;毕竟,该应用程序已具有对 Azure AD 租户的访问权限。

我们现在有了外部存储库,可以使用它来创建源验证的自定义 VINR 实现了。

在项目的根目录中创建一个新文件夹 AADUtils。右键单击该文件夹,选择“添加...”,然后单击“新项目”,在左侧的类别中选择代码,单击“类”,然后将文件命名为 MultiTenantIssuerNameRegistry

添加以下 using 指令:

using System.IdentityModel.Tokens;
using System.Xml.Linq;

使新类实现 VINR;添加几个静态属性以跟踪 tenants.xml 的路径和内容,然后添加一个静态默认构造函数(我们的所有方法都将是静态方法)以在创建时初始化这些属性。

namespace ExpenseReport.AADUtils
{
    public class MultiTenantIssuerNameRegistry: ValidatingIssuerNameRegistry
    {
        private static XDocument doc;
        private static string filePath;

        static MultiTenantIssuerNameRegistry()
        {
            filePath = HttpContext.Current.Server.MapPath("~/Content/tenants.xml");
            doc = XDocument.Load(filePath);
        }
    }
}

添加用于探测存储库中是否存在 tenantID 或密钥的方法:

public static bool ContainsTenant(string tenantId)
{
    return 
    doc.Descendants("tenant").Where(x => x.Attribute("id").Value == tenantId).Any();
}

public static bool ContainsKey(string thumbprint)
{
    return 
    doc.Descendants("key").Where(x => x.Attribute("id").Value == thumbprint).Any();
}
note备注
这两个方法基于 LINQ 的简洁语法并遵循同一原则:它们选择目标类型为“租户”或“密钥”的所有元素,并验证是否有一个元素与输入值匹配;如果有,则对结果集调用 Any() 时将返回 true。

最后,你可以添加重写验证逻辑的代码,如下所示:

protected override bool IsThumbprintValid(string thumbprint, string issuer)
{
    string issuerID = issuer.TrimEnd('/').Split('/').Last();

    if (ContainsTenant(issuerID))
    {
        if (ContainsKey(thumbprint))
            return true;
    }
    return false;
}

WIF 收到令牌时将自动调用此方法;此方法的逻辑非常简单,它验证颁发租户是否存在于列表中,以及令牌是否已使用注册的证书进行签名。

你将需要向 MultiTenantIssuerNameRegistry 添加更多的方法,但就验证来说,类现在已具有所有必需内容。

若要确保在身份验证管道的适当阶段激活它,需要将它添加到 Web.config 中以取代默认的 IssuerNameRegistry

打开 Web.config 文件,找到 <issuerNameRegistry> 元素,并将它替换为以下内容:

<issuerNameRegistry type="ExpenseReport.AADUtils.MultiTenantIssuerNameRegistry, ExpenseReport" />

第一个演练在应用中引入了某种逻辑,该逻辑在应用程序每次启动时更新颁发者对等项(用于验证令牌的签名的 X.509 证书的颁发者和指纹)。你可以在“添加自动元数据刷新”一节中找到所有详细信息。

由于我们将租户和密钥信息保存在 Web.config 以外的文件中,因此不必再在 Application_Start() 事件中执行更新:不过,这在应用的生命周期中的这个时候是很适合的,因此在本教程中我们会将它保存在那里。

然而,我们需要解决几个细节问题:

  • ValidatingIssuerNameRegistry.WriteToConfig() 方法使用原始配置条目,并需要该条目位于 Web.config 中:它不会使用我们的自定义 MTINR。

  • 已接受租户的列表不再来自单个租户的元数据

这两个问题都可以很简单地解决。你只需将另一个静态方法添加到 MTINR,该方法直接更新 tenants.xml 中的密钥。以下是该方法的代码,可将它添加到对应的 MultiTenantIssuerNameRegistry.cs 文件的 MultiTenantIssuerNameRegistry 类中。

public static void RefreshKeys(string metadataAddress)
{
    IssuingAuthority ia = 
           ValidatingIssuerNameRegistry.GetIssuingAuthority(metadataAddress);

    bool newKeys = false;
    foreach (string thumbp in ia.Thumbprints)
        if (!ContainsKey(thumbp))
        {
            newKeys = true;
            break;
        }

    if (newKeys)
    {                
        XElement keysRoot = 
             (XElement)(from tt in doc.Descendants("keys") select tt).First();
        keysRoot.RemoveNodes();
        foreach (string thumbp in ia.Thumbprints)
        {
            XElement node = new XElement("key", new XAttribute("id", thumbp));
            keysRoot.Add(node);
        }
        doc.Save(filePath);           
    }
} 

该方法可以分解为三个任务:

  • 调用 GetIssuingAuthority()(由 ValidatingIssuerNameRegistry 提供的静态方法)时,将从元数据文档中提取由服务公布的密钥,密钥形式适用于验证传入令牌

  • 第一个 foreach 块检查在元数据文档中找到的密钥是否已存在于 tenants.xml 中。

  • 如果未在元数据中找到新信息,该方法将退出而不进行任何更改;否则,将清除当前 tenants.xml 中的密钥,代之以元数据文档中的新密钥。

note备注
第一个演练中“添加自动元数据刷新”一节提供的有关 HTTPS 终结点验证的警告在此处也适用。

这是实现自动密钥刷新所需的所有逻辑:下一步是修改 Global.asax 以调用 RefreshKeys 而不是基于 Web.config 的例程。找到 Global.asax 并修改 RefreshValidationSettings,如下所示:

protected void RefreshValidationSettings()
{
    string metadataAddress = 
           ConfigurationManager.AppSettings["ida:FederationMetadataLocation"];
    AADUtils.MultiTenantIssuerNameRegistry.RefreshKeys(metadataAddress);
}
note备注
你可能已经注意到,该方法仍依赖于 Windows Azure AD 租户(即,首先开发该应用程序时所在的租户,也是首次运行标识和访问工具时捕获的租户)的元数据地址,并不移到与租户无关的终结点。实际上,这两种方法大致等效:Azure AD 中的应用程序条目已与你的租户绑定,因此可保证相应的元数据终结点正常工作。而另一方面,移到与租户无关的终结点后,可以更方便地将代码作为其他应用程序的起点来重用。你可以选择这两种方法中的任意一种,本教程选择了需要最少代码更改的那种方法。

在经典的 LoB 应用程序中,所有用户均来自同一机构,因此可以随时通过身份验证流程明确地确定需要将未经身份验证的用户发送到哪里。而且,用户通常已与机构建立活动会话(例如,用户已在本地 AD 域中通过工作站提示窗口登录),因此身份验证阶段可以透明地进行,在浏览器中键入应用的地址后就可以访问该应用的 UI,这期间你不会察觉到任何中断。这是配置为使用登录协议(如 WS-Federation)的 Web 应用的默认行为。

在多租户应用程序中,用户可以如定义的那样来自不同租户:在确定哪个机构应参与身份验证时,用户需要参与这一过程,该过程在文献中称为本地域识别 (HRD)。在 Azure AD 中,该阶段由负责引导用户完成所需体验的与租户无关的终结点处理:不需要针对该阶段更改代码。尽管如此,除 HRD 之外,其他方面也可能需要进行一些特定的工作,如下图所示。

多租户应用程序通常为匿名用户提供可用体验:常见的示例包括登录页、加入功能、新闻、公共支持论坛、免费试用的入口点等。全面保护方法不适合提供此类功能的支持:因此,本节将显示如何更改 WIF 设置以按控制器应用更精细的身份验证要求。

更改 Web.config 文件

  1. 打开 Web.config,并在标记为“由标识和访问 VS 包注释”的注释块正下方的 <system.web> 中找到 <authorization> 元素。

    note备注
    不要将该元素与 <location> 块中的同名元素混淆。

  2. 当前设置(<deny users="?" />)告诉 ASP.NET,只有通过身份验证的用户才能请求此应用程序中的资源。我们不再需要从 Web.config 驱动此约束:请注释掉整个授权元素。

    <!--<authorization>
      <deny users="?" />
    </authorization>-->
    
    我们想要的是有机会直接管理身份验证的处理方式。我们将通过还原到窗体身份验证来实现这个目标:我们会告知 ASP.NET,当必须进行身份验证时,应将其重定向到特定地址;然后,我们会使用新的负责触发联合身份验证的控制器来处理该地址的请求。

  3. Web.config 中找到 <authentication> 元素,将它注释掉,然后在你刚创建的注释块的正下方粘贴以下元素。

    <!--<authentication mode="None" />-->
    <authentication mode="Forms">
      <forms loginUrl="~/Account/LogOn" timeout="2880" />
    </authentication>
    
  4. 向下滚动到 <system.webServer> 元素,并找到 <modules> 列表。你会看到有一个针对 FormsAuthentication 模块的 <remove> 指令:请注释掉该指令,如下所示。

    <modules>
          <!--<remove name="FormsAuthentication" />-->
    
  5. 最后,向下滚动到 Web.config 的结尾。在 <wsFederation> 元素(你已在本教程的前面部分对其进行了修改)中,将 passiveRedirectEnable 设为 false

    <wsFederation passiveRedirectEnabled="false" 
    issuer="https://login.windows.net/common/wsfed" realm="https://<your-tenant-name>.onmicrosoft.com/ExpenseReport" requireHttps="false" />
    
    note备注
    默认情况下,WIF 模块将检查应用的每个 401 返回代码,如果已正确配置机构,它会将这些返回代码转换成 302 重定向,从而将登录消息发送到受信任的机构。这最后一个设置告诉 WIF 忽略传出 401,使它们可以改为根据窗体身份验证设置(此示例则根据我们的自定义控制器)进行重定向。

创建帐户控制器

  1. 现在,此应用已配置为在身份验证时重定向到 ~/Account/LogOn。让我们创建一个将处理该特定路由的控制器。

    右键单击“控制器”文件夹,依次单击“添加”“添加控制器”,然后将其命名为 AccountController

  2. 添加以下 using 指令:

    using System.IdentityModel.Services;
    
  3. 删除该控制器的默认实现,并将其替换为以下代码:

    public void LogOn()
    {
        RedirectResult result;
        if (!Request.IsAuthenticated)
        {
            SignInRequestMessage sirm = FederatedAuthentication.WSFederationAuthenticationModule.CreateSignInRequest("", HttpContext.Request.RawUrl, false);
            result = Redirect(sirm.RequestUrl.ToString());               
        }
        else
        {
            result = Redirect("~/");
        }
        result.ExecuteResult(this.ControllerContext);
    }
    
    

简而言之:该控制器执行的是与 WIF 模块所实现的逻辑几乎相同的逻辑,即:

  • 如果请求未经过身份验证:

    • 它会根据配置设置(<wsFederation> 元素的内容)生成 WS-Federation 登录消息

    • 它相应地重定向用户的浏览器

  • 如果请求已经过身份验证:

    • 它重定向到主页。

它的优点是你现在可以完全控制其发生时间:你将在本教程的后面部分看到,这样一来,你就可以在应用程序的 UI 中添加显式登录手势,同时仍然可以利用所请求资源的身份验证策略触发的自动重定向。

note备注
本演练选取了身份验证体验完全由 Azure AD 处理的最低要求方法,但这并不一定意味着你也必须在你的解决方案中这样做。你现在可完全控制触发身份验证流程时发生的情况:如果你想要显示视图以便提示(或者只是帮助)用户输入更多信息,你可以通过修改登录操作轻松地做到这一点。

若要实现完全的联合身份验证形式的身份验证集成,还有最后一件事要做。

当调用登录操作以响应针对受保护资源的请求时,窗体身份验证模块将在 ReturnUrl 查询参数中提供源资源的 URL,但 WIF 会忽略它。照现在这种情况,用户在经过身份验证后将登录到应用程序的主页上,而不是登录到所请求的资源。执行到资源的额外内部重定向很容易,只需在 Global.asax 中添加一些用于处理 End_Request 事件的逻辑即可。

  1. 打开 Global.asax 并添加以下指令:

    using System.Security.Claims;
    
  2. 然后,添加以下方法:

    protected void Application_EndRequest(object sender, EventArgs e)
    {
        string wsFamRedirectLocation = HttpContext.Current.Response.RedirectLocation;
        if (wsFamRedirectLocation != null && 
            wsFamRedirectLocation.Contains("ReturnUrl") && 
            ClaimsPrincipal.Current.Identity.IsAuthenticated)
        {
            HttpContext.Current.Response.RedirectLocation =
                        HttpUtility.ParseQueryString(
                          wsFamRedirectLocation.Split('?')[1])["ReturnUrl"];
        }
    }
    

该方法在 HTTP 请求处理管道的末端运行。它会检查在重定向时使用的 HTTP 标头“Location”:如果该标头为非空,则说明用户已经过身份验证(因此,WS-Federation 协议流已完全发生),并且它包含 ReturnUrl(指示它存储着窗体身份验证返回信息),然后浏览器就会被重定向到 ReturnUrl 位置。

保护控制器

由于你关闭了全面身份验证保护,每个控制器将负责指定各自的身份验证要求。要做到这一点,最简单的方法是使用 [Authorize] 特性值修饰每个操作,就像你对任何其他 ASP.NET 身份验证方案所做的一样。

Warning警告
虽然本教程介绍的是 MVC 样式应用程序,但完全可以使用 Web 窗体中的同样方法(例如,你可以利用 Web.config 中的 <location><authorization> 元素)。

在下面,你可以看到应用于 Home 控制器的 About() 操作的修改:

[Authorize]
public ActionResult About()
{
    ViewBag.Message = "Your app description page.";

    return View();
}

请对 Home 控制器中除 Index() 以外的每个操作进行相同的修改。 在本教程中,为未经身份验证的入口点保留了索引以获取应用程序的体验。

在第一个演练中,你修改了用于从声明中提取用户信息的索引:由于未经身份验证的用户将能够访问操作,该代码将失败,因为缺少经过身份验证的用户意味着没有可用的声明。请将其注释掉,并将分配的 ViewBag.Message 替换为你认为合适的任何消息。

Public ActionResult Index()
{
    //ClaimsPrincipal cp = ClaimsPrincipal.Current;
    //string fullname =
    //       string.Format(“{0} {1}”, cp.FindFirst(ClaimTypes.GivenName).Value,
    //       cp.FindFirst(ClaimTypes.Surname).Value);
    ViewBag.Message = "Welcome to the Expense Note App";
    return View();
}
note备注
本教程并没有给出如何执行此操作的说明,但是如果你愿意,你可以向它(或关联的视图)添加一些欢迎用户和说明如何使用该应用程序的文本。对于以后的注册部分来说,这将会特别方便。

在 _Layout.cshtml 文件中更改登录手势

你到目前为止所做的更改会在用户单击 UI 元素请求通过 [Authorize] 进行保护的一个操作时,立即触发身份验证流程。但是,对全面保护未涉及的应用程序来说,为登录提供显式的 UI 手势是很常见的。

为此,我们将重温 _Layout.cshtml(我们在解释如何在“添加登录”演练中添加注销时使用过)中定义的屏幕顶部区域的用户问候语文本。 当时我们内联了 _Layout.cshtml 中的所有内容,但在本教程中,我们将需要添加更多功能,而这要求进行一些重构。

  1. 打开 _Layout.cshtml,找到登录节 <section id=”login”> 并修改它,如下所示:

    <section id=”login”>
       @Html.Partial("_LoginPartial")
       @* @if (Request.IsAuthenticated) 
        {                       
           <text> Hello, <span class=”username”>@User.Identity.Name</span>! @Html.ActionLink(“Signout”,”SignOut”, “SignOut”)</text>
        } 
        else {          
            <text>  You are not authenticated </text> 
         } *@
    </section>
    
    
  2. 完成该操作后,右键单击文件夹“视图/共享”,依次单击“添加”“视图”,将新视图命名为 _LoginPartial,选择 Razor 作为视图引擎,然后单击“添加”

  3. _LoginPartial.cshtml 的内容替换为以下代码:

    @if (Request.IsAuthenticated) 
        {                       
           <text> Hello, <span class=”username”>@User.Identity.Name</span>! @Html.ActionLink("Signout","SignOut", "SignOut")</text>
        } 
        else {          
             <ul>       
            <li>@Html.ActionLink("Sign in", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "signLink" })</li>
        </ul> 
         }
    

这是在 LoB 教程中内联的同一逻辑,唯一区别是“你未经过身份验证”文本已替换为可点击链接,点击该链接将激活 Account 控制器的 LogOn 操作。

注销控制器

不需进行任何更改即可让注销逻辑适用于多租户的情况。该控制器已配置为从 Web.config 中检索要在注销流程中使用的颁发者终结点;这意味着它现在会自动选取对你在本教程开始时应用的与租户无关的终结点所做的更改。

自定义 Graph 访问逻辑

Graph API 访问逻辑已设计为根据在描述当前用户的声明中收到的 tenantID 来构造资源终结点;因此,即使用户将开始来自多个租户,它也会继续工作。

note备注
Azure AD 的一项特性使这成为可能,那就是,不同租户的 ClientId 和客户端密钥不会变:当租户管理员授权应用访问其 Azure AD 租户时,在该租户管理员的租户中的对应新应用程序条目将与其他人的新应用程序条目具有相同的值。但是请注意,这并不意味着一个恶意管理员可以检索应用的密钥,然后尝试对另一个向同一应用授权的 Azure AD 租户使用该密钥。正如你在 Graph API 演练中了解到的,密钥在创建后无法进行检索。

完成到多租户的转换时需要向应用添加的最后一个功能是加入新客户组织的能力。

正如文档中所说明的,在 Azure AD 中标记为可从外部使用的应用程序将关联到一个特制的 URL,潜在客户可以使用该 URL 授权应用访问其目录,并将应用的条目自动设置到其租户中。在本节中,你将添加一个 SignUp 控制器,该控制器可以将用户重定向到同意 URL 以及用于触发操作的 UI 手势。

note备注
在这里我们直接在应用中添加该功能,但是一旦你了解该机制如何工作,你便可以在该应用程序之外实现同意逻辑、使用项目组合中其他应用的同意链接为功能分组,等等。

  1. 添加一个空的 SignUp 控制器,并按照本演练前面部分提供的控制器创建说明进行操作。

  2. 添加以下 using 指令:

    using System.Configuration;
    
    
  3. 使用以下两个方法来替换该控制器的默认实现:

    private string CreateConsentURL(string clientId, string requestedPermissions, 
                                    string consentReturnURL, string context)
    {
        string consentUrl = string.Format("https://account.activedirectory.windowsazure.com/Consent.aspx?ClientId={0}", clientId);
        if(!String.IsNullOrEmpty(requestedPermissions))
          consentUrl+= "&RequestedPermissions="+requestedPermissions;
        if(!String.IsNullOrEmpty(consentReturnURL))
            consentUrl+= "&ConsentReturnURL="+HttpUtility.UrlEncode(
                consentReturnURL+(String.IsNullOrEmpty(context) ?
                                         String.Empty : "?"+context ));
        return consentUrl;        
    }
    public void SignUp()
    {
        string request = System.Web.HttpContext.Current.Request.Url.ToString();
        string returnurl = request.Substring(0, request.Length -6);
        string clientID = ConfigurationManager.AppSettings["ClientId"];
    
        RedirectResult result = 
           Redirect(CreateConsentURL(clientID, "DirectoryReaders", returnurl, string.Empty));
        result.ExecuteResult(this.ControllerContext);
    } 
    
    

第一个方法创建 URL,潜在客户可利用该 URL 授权应用访问其目录租户。虽然 Azure 管理门户会为你提供应用的同意 URL,但使用可以从一些基本参数生成该 URL 的代码的灵活性更强。让我们查看一下参数及其语义:

  • ClientId 表示应用程序的标识符。在 Graph API 演练期间,你将它添加到了配置中。它是必需的。

  • RequestedPermissions 表示你想要潜在客户授予应用程序的目录访问级别。可能值反映了你在应用注册时在 Azure 管理门户中提供的访问级别选项,不过在这里你可以自由地请求与你在 UI 中为自己的租户选取的级别不同的级别。这些值如下:

    • DirectoryReaders:使用此值请求对目录的读取访问权限

    • DirectoryWriters:使用此值请求读/写访问权限

    • <Empty>:如果你只想拥有 SSO 功能,则不指定任何值

  • ConsentReturnUrl 指示在用户授予(或拒绝)对应用程序的权限后将浏览器会话重定向到的位置。你将需要在应用中指定注册处理逻辑驻留的路径,稍后详述。

方法 CreateConsentURL 公开上述所有参数,再加上一个额外的参数。上下文参数为你提供了包含额外信息的方法,该参数将发送到 ConsentReturnUrl 中编码的同意页,并在重定向完成后返回。它作为单独的参数提供以方便你使用,这样,你便无需将主返回 URL 与上下文逻辑混合在一起了。

当你需要跟踪与请求相关的附加信息时,上下文可以派上用场。例如,在你自己的应用特定的加入过程中,经常会执行重定向到同意页这种操作,并且你可能需要跟踪一些上下文。

第二个方法是将触发重定向的实际操作。它将调用 CreateConsentURL,并将配置中的 ClientId 作为参数传递,将当前控制器的根作为返回 URL,将 DirectoryReaders 作为所请求的访问级别,而未将任何内容作为上下文。然后,该方法将继续执行重定向到新制作的 URL。

现在,你已添加用于触发重定向到同意 URL 的逻辑,你需要提供用于处理结果的代码来完成该流程。只要选择了下面的 ReturnURL,本教程就会将该代码放在 SignUp 控制器的 Index 操作中。

  1. 添加以下 using 指令。

    using ExpenseReport.AADUtils;
    
  2. 将以下方法添加到 SignUp 控制器。

    public void Index()
    {
        if ((!string.IsNullOrEmpty(Request.QueryString["TenantId"]) && 
            (!string.IsNullOrEmpty(Request.QueryString["Consent"]))))
        {
            if (Request.QueryString["Consent"].Equals("Granted",
                                                StringComparison.InvariantCultureIgnoreCase))
            {
                MultiTenantIssuerNameRegistry.AddTenant(Request.QueryString["TenantId"]);
            }
        
            //redirect to SignIn
            RedirectResult result = Redirect("~/Account/LogOn");
            result.ExecuteResult(this.ControllerContext);
        }
    }
    
    note备注
    该方法代码未包括错误管理。

授予或拒绝访问后,Azure AD 完成重定向到 ReturnURL 并追加几个额外的参数:

  • Consent:指示操作的结果。其值可以为“授予”或“拒绝”

  • TenantID:如果 Consent 参数的值为“授予”,则 TenantID 将存在并包含刚同意应用程序访问请求的租户的标识符

此方法的目的则包括捕获设置该应用的租户的 ID,并在 MTINR 集合中将这些租户记录为有效机构。按照在本演练前面部分创建的验证逻辑,这将允许在登录时接受来自新租户的用户。

上面的代码用于检查请求 URL,如果它发现请求 URL 包含新租户,则会将该租户保存在 MTINR 中(使用我们马上要实现的方法)。

note备注
此处的实现未对到达此路径的请求添加任何限制,如果请求采用正确的格式,则最终会将 TenantID 参数的内容置于 MTINR 存储中。这不是你想要在实际应用程序中实施的行为,因为滥用该行为可能会使存储中虚假条目成灾,也可能会将该行为滥用于获取未授权的访问。

如前所述,同意体验可能会是你自己的加入过程(例如,收集有关潜在客户的信息、获取付款,或者你的应用要求的任何其他加入步骤)的一部分。因此,你应该确保在创建同意 URL 时包括该上下文并在处理响应时验证该上下文;这可确保系统拒绝欺诈请求。

保存租户后,该方法最后将触发登录操作,用户可以立刻访问该应用。

note备注
如果加入过程需要执行进一步的步骤,你可以选择将登录推迟到稍后进行,并改为提供视图。

SignUp 控制器的 Index 操作引入了一个新的 MTINR 方法 AddTenant。让我们为它添加实现。打开 MultiTenantIssuerNameRegistry.cs,然后将以下方法添加到 MultiTenantIssuerNameRegistry 类中:

public static void AddTenant(string tenantId)
{
    if (!ContainsTenant(tenantId))
    {
        XElement node = new XElement("tenant", new XAttribute("id", tenantId));
        XElement tenantsRoot = 
                (XElement)(from tt in doc.Descendants("tenants") select tt).First();

        tenantsRoot.Add(node);
        doc.Save(filePath);
    }
}

AddTenant 方法非常简单:如果输入中的 tenantId 尚未存在于存储中(同意操作是幂等操作),此方法将在 <tenants> 节点下为它添加一个新条目。

最后,应用程序需要用于触发注册流程的 UI 手势。这可以通过在 _LoginPartial.cshtml 中添加条目来轻松实现:打开该文件,并更改用于注册的 <li> 块,如下所示。

@if (Request.IsAuthenticated) 
    {                       
       <text> Hello, <span class="username">@User.Identity.Name</span>! @Html.ActionLink("Signout","SignOut", "SignOut")</text>
    } 
    else {          
         <ul>  
       <li>@Html.ActionLink("Sign up", "SignUp", "SignUp", routeValues: null, htmlAttributes: new { id = "signupLink" })</li>     
        <li>@Html.ActionLink("Sign in", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "signLink" })</li>
    </ul> 
     }

note备注
当前的 UI 采用非常简约的方法。在实际的应用程序中,你可能需要设置注册的语义和登录控件:实现这一设置的可能方法包括提供更详细的链接文本、在主页的主体中放置说明,以及各种其他方法。

在本节中,我们将为你提供一些选项,你可以利用这些选项来测试新的多租户应用程序。

若要在所有方面对多租户应用程序进行测试,最好有第二个 Azure AD 租户可用,你可以使用它来执行示例应用程序中所有与同意相关的流程。在此时间点,这意味着你需要访问另一个 Azure 订阅。你可以通过第二次执行在第一个演练的开头所述的注册和目录创建流程来获取它;此外,你还可以按照此处的说明使用现有 Azure AD 租户中的用户来注册试用订阅。

鉴于授权同意操作是幂等操作,你也可以使用自己的 Azure AD 租户进行注册试验:这将使你不必执行创建第二个订阅的任务,但是,当你尝试使用未设置的租户登录或体验撤销同意流程时,将不允许你查看发生了什么情况(鉴于对于你的租户来说,该应用仍是 LoB 应用程序,因此进行了显式设置,而不特别需要授权同意步骤)。

此外,进行试验的另一种方法是使用不一定与 Azure 订阅有关的 Azure AD 租户,例如你在订阅 Office365 或 Intune 等服务时获得的目录租户。

note备注
只有 Azure AD 租户管理员才能向应用程序授予许可。无论你选择哪种测试方法,你都需要有权访问租户管理员用户凭据。如果你使用基于 Live ID 的 Azure 订阅创建了 Azure AD 租户,则需要确保你的租户至少包含一个属于“全局管理员”角色的租户用户。你可以按照第一个演练中的说明进行操作,轻松创建一个该用户,请确保你在用户配置文件屏幕上选择了“全局管理员”。

你现在终于能够看到运行中的多租户应用程序了。如果你想要观察应用的内部执行情况,可以在本教程所添加的任何代码片断中随意添加断点。

运行应用程序

note备注
如果你使用的是你自己的以外的 Azure AD 租户,则可能需要在开始之前关闭所有浏览器实例;或者,你可以指示 Visual Studio 在非默认 Web 浏览器中启动调试会话。若要确保现有 Web 会话与调试会话之间不存在干扰,这些应急方法可能是必要的。

在 Visual Studio 中,按 F5

应用程序主页

正如预期的那样,系统并未立即将你重定向到 Azure AD 进行身份验证,而是向你显示了应用程序的主根目录。

你可以在顶栏中找到登录部分,其中正确显示了“注册”和“登录”按钮,因为我们目前未经过身份验证。

让我们测试加入流程。单击“注册”按钮。

登录到 AAD

你将立即重定向到 Azure AD 的身份验证提示。使用你决定用于测试的 Azure AD 租户管理员凭据登录。

授予访问权限

note备注
如果你在此处收到错误,请再次确认你是否要以 Azure AD 租户管理员身份登录。目前,Live ID 用户不能用于在此处所述的流程中授权同意。

你成功通过身份验证后,Azure AD 会立即提示你为 Expense Reporting 应用程序授予或拒绝对你的目录租户的访问权限,以获取在 SignUp 控制器中指定的访问级别。

单击“授予”

应用程序主页

该页将在当前用户的目录中记录同意,然后触发登录;鉴于你已在同意时向 Azure AD 进行身份验证,你无需再次输入凭据,你将发现自己已自动登录。登录部分将反映此新状态,显示你的用户名和“注销”按钮。

你可以单击应用中的任意位置以确认你处于经过身份验证的会话中:例如,单击“用户”选项卡:你将能够看到用户列表。

note备注
当多租户应用程序尝试获取令牌以访问最近同意该应用程序的 Windows AD 租户的 Graph API 时,令牌请求可能失败,并出现 ACS50012 错误。若要解决该问题,请等待几分钟,然后重试。或者,让提供同意信息的租户管理员在同意之后登录应用程序。有关详细信息,请参阅 ACS 错误代码

让我们测试注销功能。单击“注销”按钮。

已注销

经过几次重定向后,你将登陆到“注销”视图上。

若要确认你确实已注销,请再次单击“用户”选项卡:这次系统将提示你登录。

输入你的凭据:你将注意到,经过身份验证阶段重定向后,浏览器将正确地显示你起初请求的用户 UI。这表明,在 End_Request 事件中添加的逻辑正常工作。

撤消访问权限

仅当你在你自己的以外的 Azure AD 租户中设置了应用程序并关联到 Azure 订阅时,才能执行本节中所示的任务。

导航到 Azure 管理门户,并以用于设置应用程序的 Azure AD 租户管理员身份登录。

转到 Active Directory 选项卡,选取你的目录,选择“应用程序”,找到 Expense Reporting 应用并单击它。

集成的应用程序详细信息

而在创建该应用的租户中,你获得了用于更改应用设置的配置 UI,在此处你扮演购买了该应用程序的客户的角色:你将获得一个一般描述页,概述主要的应用程序对等项和授予的访问级别。

单击底部命令栏中的“管理访问权限”按钮。

管理访问权限

如果你不想再让应用能够访问已授权条款中的目录,此 UI 允许你撤消应用的访问权限。

单击“删除”

已删除访问权限

该应用已删除,并不再位于“应用程序”列表中。

回到 Visual Studio,并再次按 F5

单击“登录”,然后以客户租户管理员身份进行身份验证,看看会发生什么情况。

ACS50000 错误

该用户向目录成功地进行身份验证,但作为身份验证令牌接收者的应用将不再作为有权访问该用户的目录租户的应用列出:其结果是,目录将不会颁发所请求的令牌,并向用户显示错误页。

社区附加资源

显示:
© 2015 Microsoft