演练:多租户服务器到服务器身份验证

 

发布日期: 2017年1月

适用于: Dynamics 365 (online)

此演练介绍创建可使用 Microsoft Visual Studio 2015 MVC Web 应用程序模板连接到 适用于 Microsoft Dynamics 365 的 2016 年 12 月更新(联机) 租户的多租户 Web 应用程序的步骤。

要求

  • 已安装了 Web 开发人员工具 的 Visual Studio 2015

  • 与您的 Azure Active Directory (Azure AD) 租户关联的 适用于 Microsoft Dynamics 365 的 2016 年 12 月更新(联机) 租户。

  • 与其他 Azure AD 租户关联的第二个 适用于 Microsoft Dynamics 365 的 2016 年 12 月更新(联机) 租户。 此租户代表您的应用程序的订户。 这可以是 适用于 Microsoft Dynamics 365 的 2016 年 12 月更新(联机) 试用订阅。

本演练的目标

完成此演练时,您将获得一个 MVC Web 应用程序,该应用程序使用 WhoAmIRequest Class 检索有关该应用程序用于连接到 Dynamics 365(在线) 租户的用户的数据。

成功运行应用时,将在右上角中看到“登录”命令。

The sign in command in the app

单击“登录”命令,这样将把您定向到您的凭据的 Azure AD。

登录后,您将看到一条“WhoAmI”命令。

The WhoAmI command

单击“WhoAmI”,您应该会看到以下信息:

Results of a WhoAmI request

当您查询您的 Dynamics 365 租户时,您将看到 WhoAmI 消息返回的结果引用您已为 Web 应用程序配置来使用的特定应用程序用户帐户,而不是您正在使用的用户帐户。

验证 Azure AD 租户

首先,连接到您的 Office 365 管理中心https://portal.office.com,然后在“管理中心”下拉菜单中,验证是否可以同时看到“Dynamics 365”和“Azure AD”。

Admin Centers with Azure Active Directory and Dynamics 365

如果您的 Azure AD 订阅未与 Dynamics 365 订阅关联,则您不能为应用程序授予访问 Dynamics 365 数据的权限。

如果您未看到此选项,请参阅注册免费 Azure Active Directory 订阅以获取有关如何注册以获取 Azure AD 订阅的信息。

如果您已经有 Azure 订阅,但该订阅尚未与您的 Microsoft Office 365 帐户关联,请参阅关联 Office 365 帐户与 Azure AD 以创建和管理应用。

创建 MVC Web 应用程序

您可以通过使用 Visual Studio 2015 新建 MVC Web 应用程序并在您的 Azure AD 租户中注册。

  1. 打开“Visual Studio 2015”。

  2. 确保您用于登录的 Microsoft 帐户 是可访问您希望用于注册应用程序的 Azure AD 租户的同一个。

  3. 单击“新建项目”,然后选择“ .NET Framework 4.6.1”和“ASP.NET Web 应用程序”模板。

    单击“确定”,然后在“新建 ASP.NET 项目”对话框中,选择“MVC”。

  4. 单击“更改身份验证”按钮,然后在对话框中选择“工作和学校帐户”。

  5. 在下拉列表,选择“云 - 多个组织”。

    ASP.NET MVC Change Authentication Dialog

  6. 单击“确定”并完成项目的初始化。

    备注

    按照这种方法创建 Visual Studio 项目将在您的 Azure AD 租户中注册应用程序,并向 Web.Config appSettings 添加以下密钥:

    <add key="ida:ClientId" value="baee6b74-3c39-4c04-bfa5-4414f3dd1c26" />
    <add key="ida:AADInstance" value="https://login.microsoftonline.com/" />
    <add key="ida:ClientSecret" value="HyPjzuRCbIl/7VUJ2+vG/+Gia6t1+5y4dvtKAcyztL4=" /> 
    

在 Azure AD 中注册应用程序

如果您已经执行了 创建 MVC Web 应用程序 中的步骤,则应发现您在 Visual Studio 中创建的 Web 应用程序项目已在您的 Azure AD 应用程序中注册。 但还必须在 Azure AD 门户中执行一个步骤。

  1. 转到 https://portal.azure.com 并选择“Azure Active Directory”。

  2. 单击“应用注册”,然后查找您使用 Visual Studio 创建的应用程序。 在“常规”区域中,验证属性:

    Application registration data in Azure Active Directory

  3. 验证“应用程序 ID”属性是否匹配 Web.Config appSettings 中添加的 ClientId 值。

  4. “主页 URL”值应该匹配您的 Visual Studio 项目中的 SSL URL 属性,并且应该定向到 localhost URL,如 https://localhost:44392/。

    备注

    以后您真正发布应用程序时,需要更改此值。 但是您需要将其设置为正确的 localhost 值来进行调试。

  5. 您需要为应用程序授予访问 Dynamics 365 数据的权限。 在“API 访问权限”区域中,单击“必需权限”。 您应该发现它已经有了 Windows Azure Active Directory 的权限。

  6. 单击“添加”,然后单击“选择 API”。 在列表中,选择“Dynamics 365”,然后单击“选择”按钮。

  7. 在“选择权限”中,选择“作为组织用户访问 Dynamics 365”。 然后单击“选择”按钮。

  8. 单击“完成”添加这些权限。 完成后,应该看到应用的权限:

    Dynamics 365 permissions applied to application in Azure Active Directory

  9. 在“ API 访问权限”区域中,确认已添加了一个“密钥”值。 创建应用程序后“密钥”值在 Azure 门户中不可见,但是该值已作为 ClientSecret 添加到了您的 Web.Config appSettings 中。

创建应用程序用户

通过 手动创建 Dynamics 365 应用程序用户 中的步骤,使用您的应用程序注册产生的“应用程序 ID”值(该值与 Web.Config 中的 ClientId 值相同)创建应用程序用户。

添加程序集

在项目中添加以下 NuGet 包

版本

Microsoft.CrmSdk.CoreAssemblies

最新版本

Microsoft.IdentityModel.Clients.ActiveDirectory

2.22.302111727

Microsoft.IdentityModel.Tokens

5.0.0

Microsoft.Azure.ActiveDirectory.GraphClient

2.1.0

备注

不将 Microsoft.IdentityModel.Clients.ActiveDirectory 程序集更新到最新版本。 这些程序集的版本 3.x 更改了 Microsoft.CrmSdk.CoreAssemblies 依赖的界面。

有关管理 NuGet 包的信息,请参阅 NuGet 文档:使用 UI 管理 NuGet 包

应用 MVC 模板的代码更改

以下代码更改提供基本功能,以使用 Dynamics 365WhoAmI消息和验证应用程序用户帐户身份是否正在被应用程序使用。

Web.config

将以下密钥添加到 appSettings。

<add key="ida:OrganizationHostName" value="https://{0}.crm.dynamics.com" /> 

ida:OrganizationHostName 字符串将在字符串处添加订户的 Dynamics 365 在线组织,以便访问正确服务。

<add key="owin:appStartup" value="<your app namespace>.Startup" />

owin:appStartup 字符串确保 OWIN 中间件使用此项目中的 Startup 类。 否则您将收到以下错误:

- No assembly found containing an OwinStartupAttribute.
- No assembly found containing a Startup or [AssemblyName].Startup class.

更多信息:ASP.NET:OWIN 启动类检测

Controllers/HomeController.cs

AllowAnonymous 修饰器添加到 Index 操作。 这样不经身份验证就可以访问默认页面。

using System.Web.Mvc;

namespace SampleApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        [AllowAnonymous]
        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();
        }
    }
}

备注

在您的 Web 应用程序或服务中,您不应允许匿名访问。 此处使用匿名访问只是为了简单起见。 此演练不涵盖如何控制应用程序的访问。

Views/Shared/_Layout.cshtml

若要显示已身份验证用户的命令链接“WhoAmI”,您需要编辑此文件。

找到类为 navbar-collapse collapse 的div 元素,将其编辑为包含下面的代码:

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
     <li>@Html.ActionLink("Home", "Index", "Home")</li>
     <li>@Html.ActionLink("About", "About", "Home")</li>
     <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
     @if (Request.IsAuthenticated)
     {
         <li>@Html.ActionLink("WhoAmI", "Index", "CrmSdk")</li>
     }
    </ul>

    @Html.Partial("_LoginPartial")
   </div>

App_Start/Startup.Auth.cs

新租户登录应用程序时,以下更改将调用许可框架:

public partial class Startup
 {
  private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
  private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
  //Not used   
  //private string graphResourceID = "https://graph.windows.net";    
  private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
  private string authority = aadInstance + "common";
  private ApplicationDbContext db = new ApplicationDbContext();

  //Added
  private string OrganizationHostName = ConfigurationManager.AppSettings["ida:OrganizationHostName"];

  public void ConfigureAuth(IAppBuilder app)
  {

   app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

   app.UseCookieAuthentication(new CookieAuthenticationOptions { });

   app.UseOpenIdConnectAuthentication(
       new OpenIdConnectAuthenticationOptions
       {
        ClientId = clientId,
        Authority = authority,
        TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
        {
         /*
         instead of using the default validation 
         (validating against a single issuer value, as we do in line of business apps), 
         we inject our own multitenant validation logic
         */
         ValidateIssuer = false,
        },
        Notifications = new OpenIdConnectAuthenticationNotifications()
        {
         SecurityTokenValidated = (context) =>
                  {
                   return Task.FromResult(0);
                  },
         AuthorizationCodeReceived = (context) =>
                  {
                   var code = context.Code;

                   ClientCredential credential = new ClientCredential(clientId, appKey);
                   string tenantID = context
                    .AuthenticationTicket
                    .Identity
                    .FindFirst("https://schemas.microsoft.com/identity/claims/tenantid")
                    .Value;

                   /* Not used
                  string signedInUserID = context
                     .AuthenticationTicket
                     .Identity
                     .FindFirst(ClaimTypes.NameIdentifier)
                     .Value;  
                     */

                   //Added
                   var resource = string.Format(OrganizationHostName, '*');
                   //Added
                   Uri returnUri = new Uri(
                    HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)
                    );

                   /* Changed below
                    AuthenticationContext authContext = 
                    new AuthenticationContext(
                     aadInstance + tenantID, 
                     new ADALTokenCache(signedInUserID)
                     );
                    */
                   //Changed version
                   AuthenticationContext authContext =
                   new AuthenticationContext(aadInstance + tenantID);

                   /* Changed below
                   AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                       code, 
                       new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), 
                       credential, 
                       graphResourceID);
                   */
                   //Changed version
                   AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                       code,
                       new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
                       credential,
                       resource);

                   return Task.FromResult(0);
                  },
         AuthenticationFailed = (context) =>
                  {
                   context.OwinContext.Response.Redirect("/Home/Error");
                   context.HandleResponse(); // Suppress the exception
                   return Task.FromResult(0);
                  }
        }
       });

  }
 }

添加 Controllers/CrmSdkController

将以下 CrmSdkController.cs 添加到 Controllers 文件夹。 此代码将执行 WhoAmI 消息。

  1. 右键单击 Controllers 文件夹,然后选择“添加”>“控制器...”

  2. 在“添加基架”对话框中,选择“MVC5 控制器 - 空”

  3. 单击“添加”

  4. 粘贴用于将 <Your app namespace> 替换为您的应用的命名空间的以下代码。

using Microsoft.IdentityModel.Clients.ActiveDirectory; 
using Microsoft.Xrm.Sdk; 
using Microsoft.Xrm.Sdk.WebServiceClient; 
using System; using System.Configuration; 
using System.Linq; 
using System.Security.Claims; 
using System.Web.Mvc;

namespace <Your app namespace>
{
 [Authorize]
 public class CrmSdkController : Controller
    {

  private string clientId = 
   ConfigurationManager.AppSettings["ida:ClientId"];
  private string authority = 
   ConfigurationManager.AppSettings["ida:AADInstance"] + "common";
  private string aadInstance = 
   ConfigurationManager.AppSettings["ida:AADInstance"];
  private string OrganizationHostName = 
   ConfigurationManager.AppSettings["ida:OrganizationHostName"];
  private string appKey = 
   ConfigurationManager.AppSettings["ida:ClientSecret"];


  // GET: CrmSdk
  public ActionResult Index()
  {
   string tenantID = ClaimsPrincipal
    .Current
    .FindFirst("https://schemas.microsoft.com/identity/claims/tenantid")
    .Value;
   // Clean organization name from user logged
   string organizationName = User.Identity.Name.Substring(
    User.Identity.Name.IndexOf('@') + 1, 
    User.Identity.Name.IndexOf('.') - (User.Identity.Name.IndexOf('@') + 1)
    );
   //string crmResourceId = "https://[orgname].crm.microsoftonline.com";
   var resource = string.Format(OrganizationHostName, organizationName);
   // Request a token using application credentials
   ClientCredential clientcred = new ClientCredential(clientId, appKey);
   AuthenticationContext authenticationContext = 
    new AuthenticationContext(aadInstance + tenantID);
   AuthenticationResult authenticationResult = 
    authenticationContext.AcquireToken(resource, clientcred);
   var requestedToken = authenticationResult.AccessToken;
   // Invoke SDK using using the requested token
   using (var sdkService =
    new OrganizationWebProxyClient(
     GetServiceUrl(organizationName), false)
     )
   {
    sdkService.HeaderToken = requestedToken;
    OrganizationRequest request = new OrganizationRequest() {
     RequestName = "WhoAmI"
    };
    OrganizationResponse response = sdkService.Execute(request);
    return View((object)string.Join(",", response.Results.ToList()));
   }
  }

  private Uri GetServiceUrl(string organizationName)
  {
   var organizationUrl = new Uri(
    string.Format(OrganizationHostName, organizationName)
    );
   return new Uri(
    organizationUrl + 
    @"/xrmservices/2011/organization.svc/web?SdkClientVersion=8.2"
);
  }
 }
}

Views/CrmSdk

添加一个名称为 Index 的新视图。

  1. 右键单击 CrmSdk 文件夹,然后选择“添加”>“视图...”

  2. 在“添加视图”对话框中,设置以下值:

    MVC Add View Dialog

  3. 单击“添加”

  4. 将生成的代码替换为以下代码:

    @model string
    @{
     ViewBag.Title = "SDK Connect";
    }
    
    
    <h2>@ViewBag.Title.</h2>
    
    <p>Connected and executed sdk command WhoAmI.</p>
    
    <p>Value: @Model</p>
    

调试应用

在按 F5 调试应用程序时,可能出错,错误是访问使用 SSL 的 localhost 的证书不可信。 下面是用于解决 Visual Studio 和 IIS Express 的此问题的一些链接:

备注

对于此步骤,您只需使用与您的 Azure AD 租户关联的 Microsoft 帐户 和其关联的 Dynamics 365 租户。 这其实不能演示多租户方案。 我们将在下一步中演示。 此步骤仅验证代码在增加实际多租户功能测试复杂性之前,是否可以工作。

请参阅 本演练的目标 中所述步骤测试应用程序。

此时可以验证是否使用了应用程序用户帐户。 检查此项的简单方法是使用 Dynamics 365 Web API。 在单独的选项卡或窗口中键入以下 URL,替换应用程序提供的 UserId 值。

[Organization URI]/api/data/v8.2/systemusers(<UserId value>)?$select=fullname

JSON 响应应如下所示。 请注意,fullname 值将提供给您在 创建应用程序用户 步骤中创建的应用程序用户,而不是您用于登录应用程序的 Dynamics 365 用户。

{
        "@odata.context": "[Organization Uri]/api/data/v8.2/$metadata#systemusers(fullname)/$entity",
        "@odata.etag": "W/\"603849\"",
        "fullname": "S2S User",
        "systemuserid": "31914b34-be8d-e611-80d8-00155d892ddc",
        "ownerid": "31914b34-be8d-e611-80d8-00155d892ddc"
}

配置测试订户

由于您已经验证了应用程序可以工作,所以现在该测试与其他 Dynamics 365(在线) 租户之间的连接。 如果使用其他 Dynamics 365(在线) 组织,您需要执行以下步骤。

授予订阅租户的许可

要授予许可,请在作为 Azure AD 管理员登录时执行以下步骤:

  1. 调试应用程序时,请打开单独的 InPrivate 或假名窗口。

  2. 在窗口的地址字段中,键入应用的 URL,如 https://localhost:44392/

  3. 单击“登录”按钮,此时系统将提示您授予许可。

    Azure Active Directory consent form

当您授予许可后,将返回应用,但是还不能使用该应用。 如果此时您单击“WhoAmI”,可能遇到以下异常:

System.ServiceModel.Security.MessageSecurityException
HResult=-2146233087
  Message=The HTTP request is unauthorized with client authentication scheme 'Anonymous'. The authentication header received from the server was 'Bearer authorization_uri=https://login.windows.net/4baaeaaf-2771-4583-99eb-7c7e39aa1e74/oauth2/authorize, resource_id=https://<org name>.crm.dynamics.com/'.
InnerException.Message =The remote server returned an error: (401) Unauthorized.

通过授予许可,来自您的 Azure AD 租户的应用程序将添加到订户的活动目录租户中的应用程序。

在订户租户中创建自定义安全角色

您将需要创建的应用程序必须与用于定义其权限的自定义安全角色关联。 对于此手动测试步骤,您首先应该手动创建一个自定义安全角色。详细信息:TechNet:创建或编辑安全角色

备注

应用程序用户不能与一个默认 Dynamics 365 安全角色关联。 必须创建要与应用程序用户关联的自定义安全角色。

创建订户应用程序用户

为了达成本演练的目的,我们将手动创建应用程序用户以验证来自其他租户的连接。 当您部署到实际订户时,您需要实现此过程的自动化。详细信息:准备方法以部署应用程序用户

您可以手动使用用于 创建应用程序用户 中的开发组织的相同值创建应用程序用户。 除了必须已完成首先授予许可的步骤。 保存用户时,将设置“应用程序 ID URI”和“Azure AD 对象 ID”值。 如果您尚未首先授予许可,则不能保存用户。

最后,将应用程序用户与您在上一步中添加的自定义安全角色关联。

测试订户连接

除了将凭据用于来自另一个 Dynamics 365 租户的用户,请重复 调试应用 中的步骤。

另请参阅

使用多租户服务器到服务器身份验证
使用单租户服务器到服务器身份验证
使用服务器到服务器身份验证 (S2S) 构建 Web 应用程序
连接到 Microsoft Dynamics 365

Microsoft Dynamics 365

© 2017 Microsoft。 保留所有权利。 版权