您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn.

教程:保护 Azure 远程渲染和模型存储Tutorial: Securing Azure Remote Rendering and model storage

在本教程中,你将了解如何执行以下操作:In this tutorial, you learn how to:

  • 保护包含 Azure 远程渲染模型的 Azure Blob 存储Secure Azure Blob Storage containing Azure Remote Rendering models
  • 使用 Azure AD 进行身份验证以访问 Azure 远程渲染实例Authenticate with Azure AD to access your Azure Remote Rendering instance
  • 使用 Azure 凭据进行 Azure 远程渲染身份验证Use Azure credentials for Azure Remote Rendering authentication

先决条件Prerequisites

为什么需要更多安全性Why additional security is needed

应用程序的当前状态及其对 Azure 资源的访问权限如下所示:The current state of the application and its access to your Azure resources looks like this:

初始安全性

“AccountID + AccountKey”和“URL + SAS 令牌”都同时存储了用户名和密码。Both the "AccountID + AccountKey" and the "URL + SAS Token" are both essentially storing a username and password together. 例如,如果公开了“AccountID + AccountKey”,攻击者在未经你允许的情况下使用你的 ARR 资源是很容易的,且由你承担后果。For example, if the "AccountID + AccountKey" were exposed, it would be trivial for an attacker to use your ARR resources without your permission at your expense.

在 Azure Blob 存储中保护内容Securing your content in Azure Blob Storage

通过正确的配置,Azure 远程渲染可以安全访问 Azure Blob 存储的内容。Azure Remote Rendering can securely access the contents of your Azure Blob Storage with the correct configuration. 请参阅操作说明:链接存储帐户,了解如何使用 blob 存储帐户配置 Azure 远程渲染实例。See How-to: Link storage accounts to configure your Azure Remote Rendering instance with your blob storage accounts.

使用链接的 blob 存储时,可以使用略微不同的方法来加载模型:When using a linked blob storage, you'll use slightly different methods for loading models:

var loadModelParams = new LoadModelFromSasOptions(modelPath, modelEntity);
var task = ARRSessionService.CurrentActiveSession.Connection.LoadModelFromSasAsync(loadModelParams);

上述行使用 FromSas 版本的参数和会话操作。The above lines use the FromSas version of the params and session action. 必须将它们转换为非 SAS 版本:They must be converted to the non-SAS versions:

var loadModelParams = new LoadModelOptions(storageAccountPath, blobContainerName, modelPath, modelEntity);
var task = ARRSessionService.CurrentActiveSession.Connection.LoadModelAsync(loadModelParams);

让我们修改 RemoteRenderingCoordinator 以从链接的 blob 存储帐户加载自定义模型。Let's modify RemoteRenderingCoordinator to load a custom model, from a linked blob storage account.

  1. 如果尚未执行此操作,请完成操作说明:链接存储帐户,向 ARR 实例授予访问 Blob 存储实例的权限。If you haven't already, complete the How-to: Link storage accounts to grant your ARR instance permission to access your Blob Storage instance.

  2. 将以下已修改的 LoadModel 方法添加到当前 LoadModel 方法正下方的 RemoteRenderingCoordinator 中 :Add the following modified LoadModel method to RemoteRenderingCoordinator just below the current LoadModel method:

    /// <summary>
    /// Loads a model from blob storage that has been linked to the ARR instance
    /// </summary>
    /// <param name="storageAccountName">The storage account name, this contains the blob containers </param>
    /// <param name="blobContainerName">The blob container name, i.e. arroutput</param>
    /// <param name="modelPath">The relative path inside the container to the model, i.e. test/MyCustomModel.arrAsset</param>
    /// <param name="parent">The parent Transform for this remote entity</param>
    /// <param name="progress">A call back method that accepts a float progress value [0->1]</param>
    /// <returns></returns>
    public async Task<Entity> LoadModel(string storageAccountName, string blobContainerName, string modelPath, Transform parent = null, Action<float> progress = null)
    {
        //Create a root object to parent a loaded model to
        var modelEntity = ARRSessionService.CurrentActiveSession.Connection.CreateEntity();
    
        //Get the game object representation of this entity
        var modelGameObject = modelEntity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
    
        //Ensure the entity will sync its transform with the server
        var sync = modelGameObject.GetComponent<RemoteEntitySyncObject>();
        sync.SyncEveryFrame = true;
    
        //Parent the new object under the defined parent
        if (parent != null)
        {
            modelGameObject.transform.SetParent(parent, false);
            modelGameObject.name = parent.name + "_Entity";
        }
    
    #if UNITY_WSA
        //Anchor the model in the world, prefer anchoring parent if there is one
        if (parent != null)
        {
            parent.gameObject.AddComponent<WorldAnchor>();
        }
        else
        {
            modelGameObject.AddComponent<WorldAnchor>();
        }
    #endif
    
        //Load a model that will be parented to the entity
        var loadModelParams = new LoadModelOptions($"{storageAccountName}.blob.core.windows.net", blobContainerName, modelPath, modelEntity);
        var loadModelAsync = ARRSessionService.CurrentActiveSession.Connection.LoadModelAsync(loadModelParams, progress);
        var result = await loadModelAsync;
        return modelEntity;
    }
    

    大多数情况下,此代码与原始 LoadModel 方法相同,但我们已将该方法调用的 SAS 版本替换为非 SAS 版本。For the most part, this code is identical to the original LoadModel method, however we've replaced the SAS version of the method calls with the non-SAS versions.

    还向参数添加了其他输入:storageAccountNameblobContainerNameThe additional inputs storageAccountName and blobContainerName have also been added to the arguments. 我们将从另一个方法中调用这个新的 LoadModel 方法,该方法类似于我们在第一个教程中创建的首个 LoadTestModel 方法 。We'll call this new LoadModel method from another method similar to the very first LoadTestModel method we created in the first tutorial.

  3. 将以下方法添加到 LoadTestModel 正后方的 RemoteRenderingCoordinator Add the following method to RemoteRenderingCoordinator just after LoadTestModel

    private bool loadingLinkedCustomModel = false;
    
    [SerializeField]
    private string storageAccountName;
    public string StorageAccountName {
        get => storageAccountName.Trim();
        set => storageAccountName = value;
    }
    
    [SerializeField]
    private string blobContainerName;
    public string BlobContainerName {
        get => blobContainerName.Trim();
        set => blobContainerName = value;
    }
    
    [SerializeField]
    private string modelPath;
    public string ModelPath {
        get => modelPath.Trim();
        set => modelPath = value;
    }
    
    [ContextMenu("Load Linked Custom Model")]
    public async void LoadLinkedCustomModel()
    {
        if (CurrentCoordinatorState != RemoteRenderingState.RuntimeConnected)
        {
            Debug.LogError("Please wait for the runtime to connect before loading the test model. Try again later.");
            return;
        }
        if (loadingLinkedCustomModel)
        {
            Debug.Log("Linked Test model already loading or loaded!");
            return;
        }
        loadingLinkedCustomModel = true;
    
        // Create a parent object to use for positioning
        GameObject testParent = new GameObject("LinkedCustomModel");
        testParent.transform.position = new Vector3(0f, 0f, 3f);
    
        await LoadModel(StorageAccountName, BlobContainerName, ModelPath, testParent.transform, (progressValue) => Debug.Log($"Loading Test Model progress: {Math.Round(progressValue * 100, 2)}%"));
    }
    

    此代码将另外三个字符串变量添加到 RemoteRenderingCoordinator 组件。This code adds three additional string variables to your RemoteRenderingCoordinator component. 屏幕截图,其中突出显示了“存储帐户名称”、“Blob 容器名称”和 RemoteRenderingCoordinator 组件的“模型路径”。Screenshot that highlights the Storage Account Name, Blob Container Name, and Model Path of the RemoteRenderingCoordinator component.

  4. 将值添加到 RemoteRenderingCoordinator 组件。Add your values to the RemoteRenderingCoordinator component. 按照模型转换快速入门进行操作后,你的值应为:Having followed the Quickstart for model conversion, your values should be:

    • 存储帐户名称:你的存储帐户名称,即为存储帐户选择的全局唯一名称。Storage Account Name: Your storage account name, the globally unique name you choose for your storage account. 在快速入门中,该名称为 arrtutorialstorage,你的值将有所不同。In the quickstart this was arrtutorialstorage, your value will be different.
    • Blob 容器名称:arroutput,Blob 存储容器Blob Container Name: arroutput, the Blob Storage Container
    • 模型路径:Arrconfig.json 文件中定义的“outputFolderPath”和“outputAssetFileName”的组合。Model Path: The combination of the "outputFolderPath" and the "outputAssetFileName" defined in the arrconfig.json file. 在快速入门中为 "outputFolderPath":"converted/robot"、"outputAssetFileName": "robot.arrAsset"。In the quickstart this was "outputFolderPath":"converted/robot", "outputAssetFileName": "robot.arrAsset". 其结果是得到模型路径值“converted/robot/robot.arrAsset”,你的值也会有所不同。Which would result in a Model Path value of "converted/robot/robot.arrAsset", your value will be different.

    提示

    如果运行 Conversion.ps1 脚本,而不包含“-UseContainerSas”参数,则该脚本将为你输出上述所有值,而不是 SAS 令牌。If you run the Conversion.ps1 script, without the "-UseContainerSas" argument, the script will output all of the above values for your instead of the SAS token. 链接模型Linked Model

  5. 现在,请删除或禁用 GameObject TestModel,以便为自定义模型加载提供空间。For the time being, remove or disable the GameObject TestModel, to make room for your custom model to load.

  6. 播放场景并连接到远程会话。Play the scene and connect to a remote session.

  7. 右键单击“RemoteRenderingCoordinator”并选择“加载链接的自定义模型” 。Right click on your RemoteRenderingCoordinator and select Load Linked Custom Model. 加载链接模型Load linked model

通过从本地应用程序中删除 SAS 令牌,这些步骤提高了应用程序的安全性。These steps have increased the security of the application by removing the SAS token from the local application.

现在,应用程序的当前状态及其对 Azure 资源的访问权限如下所示:Now the current state of the application and its access to your Azure resources looks like this:

更安全

还需要从本地应用程序中删除另外一个“密码”,即 AccountKey。We have one more "password", the AccountKey, to remove from the local application. 这可以使用 Azure Active Directory (AAD) 身份验证来完成。This can be done using Azure Active Directory (AAD) authentication.

Azure Active Directory (Azure AD) 身份验证Azure Active Directory (Azure AD) authentication

使用 AAD 身份验证,可以通过更可控的方式确定使用 ARR 的个人或组。AAD authentication will allow you to determine which individuals or groups are using ARR in a more controlled way. ARR 内置了对接受访问令牌的支持,而不是对使用帐户密钥的支持。ARR has built in support for accepting Access Tokens instead of using an Account Key. 可以将访问令牌看作是一个有时间限制的、特定于用户的密钥,它只解锁所请求的特定资源的某些部分。You can think of Access Tokens as a time-limited, user-specific key, that only unlocks certain parts of the specific resource it was requested for.

RemoteRenderingCoordinator 脚本具有一个名为 ARRCredentialGetter 的委托,该委托包含一个返回 SessionConfiguration 对象的方法,此方法用于配置远程会话管理。The RemoteRenderingCoordinator script has a delegate named ARRCredentialGetter, which holds a method that returns a SessionConfiguration object, which is used to configure the remote session management. 我们可以将一个不同的方法分配给 ARRCredentialGetter,这使我们可以使用 Azure 登录流,生成包含 Azure 访问令牌的 SessionConfiguration 对象。We can assign a different method to ARRCredentialGetter, allowing us to use an Azure sign in flow, generating a SessionConfiguration object that contains an Azure Access Token. 此访问令牌特定于正在登录的用户。This Access Token will be specific to the user that's signing in.

  1. 请按照如何:配置身份验证 - 已部署的应用程序的身份验证进行操作,具体来说,需要遵循 Azure 空间定位点文档 Azure AD 用户身份验证中列出的说明。Follow the How To: Configure authentication - Authentication for deployed applications, specifically you'll follow the instructions listed in the Azure Spatial Anchors documentation Azure AD user authentication. 这涉及到注册新的 Azure Active Directory 应用程序并配置对 ARR 实例的访问。Which involves registering a new Azure Active Directory application and configuring access to your ARR instance.

  2. 配置新的 AAD 应用程序后,请检查你的 AAD 应用程序是否如下图所示:After configuring the new AAD application, check your AAD application looks like the following images:

    AAD 应用程序 -> 身份验证 应用身份验证AAD Application -> Authentication App authentication

    AAD 应用程序 -> API 权限 应用 APIAAD Application -> API Permissions App APIs

  3. 配置远程渲染帐户后,请检查你的配置是否如下图所示:After configuring your Remote Rendering account, check your configuration looks like the following image:

    AAR -> AccessControl (IAM) ARR 角色AAR -> AccessControl (IAM) ARR Role

    备注

    所有者角色的权限不足以通过客户端应用程序管理会话。An Owner role is not sufficient to manage sessions via the client application. 对于要授予会话管理权限的每个用户,你需要向他们提供远程渲染客户端角色。For every user you want to grant the ability to manage sessions you must provide the role Remote Rendering Client. 对于要管理会话和转换模型的每个用户,必须为其提供远程渲染管理员角色。For every user you want to manage sessions and convert models, you must provide the role Remote Rendering Administrator.

在 Azure 端的各方面准备就绪后,现在需要修改代码连接到 AAR 服务的方式。With the Azure side of things in place, we now need to modify how your code connects to the AAR service. 为此,我们实现 BaseARRAuthentication 的实例,该实例将返回一个新的 SessionConfiguration 对象。We do that by implementing an instance of BaseARRAuthentication, which will return a new SessionConfiguration object. 在这种情况下,将使用 Azure 访问令牌配置帐户信息。In this case, the account info will be configured with the Azure Access Token.

  1. 创建一个名为 AADAuthentication 的新脚本,并将其代码替换为以下内容:Create a new script named AADAuthentication and replace its code with the following:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Identity.Client;
    using System;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using UnityEngine;
    
    public class AADAuthentication : BaseARRAuthentication
    {
        [SerializeField]
        private string accountDomain;
        public string AccountDomain
        {
            get => accountDomain.Trim();
            set => accountDomain = value;
        }
    
        [SerializeField]
        private string activeDirectoryApplicationClientID;
        public string ActiveDirectoryApplicationClientID
        {
            get => activeDirectoryApplicationClientID.Trim();
            set => activeDirectoryApplicationClientID = value;
        }
    
        [SerializeField]
        private string azureTenantID;
        public string AzureTenantID
        {
            get => azureTenantID.Trim();
            set => azureTenantID = value;
        }
    
        [SerializeField]
        private string azureRemoteRenderingAccountID;
        public string AzureRemoteRenderingAccountID
        {
            get => azureRemoteRenderingAccountID.Trim();
            set => azureRemoteRenderingAccountID = value;
        }
    
        [SerializeField]
        private string azureRemoteRenderingAccountAuthenticationDomain;
        public string AzureRemoteRenderingAccountAuthenticationDomain
        {
            get => azureRemoteRenderingAccountAuthenticationDomain.Trim();
            set => azureRemoteRenderingAccountAuthenticationDomain = value;
        }
    
        public override event Action<string> AuthenticationInstructions;
    
        string authority => "https://login.microsoftonline.com/" + AzureTenantID;
    
        string redirect_uri = "https://login.microsoftonline.com/common/oauth2/nativeclient";
    
        string[] scopes => new string[] { "https://sts." + AzureRemoteRenderingAccountAuthenticationDomain + "/mixedreality.signin" };
    
        public void OnEnable()
        {
            RemoteRenderingCoordinator.ARRCredentialGetter = GetAARCredentials;
            this.gameObject.AddComponent<ExecuteOnUnityThread>();
        }
    
        public async override Task<SessionConfiguration> GetAARCredentials()
        {
            var result = await TryLogin();
            if (result != null)
            {
                Debug.Log("Account signin successful " + result.Account.Username);
    
                var AD_Token = result.AccessToken;
    
                return await Task.FromResult(new SessionConfiguration(AzureRemoteRenderingAccountAuthenticationDomain, AccountDomain, AzureRemoteRenderingAccountID, "", AD_Token, ""));
            }
            else
            {
                Debug.LogError("Error logging in");
            }
            return default;
        }
    
        private Task DeviceCodeReturned(DeviceCodeResult deviceCodeDetails)
        {
            //Since everything in this task can happen on a different thread, invoke responses on the main Unity thread
            ExecuteOnUnityThread.Enqueue(() =>
            {
                // Display instructions to the user for how to authenticate in the browser
                Debug.Log(deviceCodeDetails.Message);
                AuthenticationInstructions?.Invoke(deviceCodeDetails.Message);
            });
    
            return Task.FromResult(0);
        }
    
        public override async Task<AuthenticationResult> TryLogin()
        {
            var clientApplication = PublicClientApplicationBuilder.Create(ActiveDirectoryApplicationClientID).WithAuthority(authority).WithRedirectUri(redirect_uri).Build();
            AuthenticationResult result = null;
            try
            {
                var accounts = await clientApplication.GetAccountsAsync();
    
                if (accounts.Any())
                {
                    result = await clientApplication.AcquireTokenSilent(scopes, accounts.First()).ExecuteAsync();
    
                    return result;
                }
                else
                {
                    try
                    {
                        result = await clientApplication.AcquireTokenWithDeviceCode(scopes, DeviceCodeReturned).ExecuteAsync(CancellationToken.None);
                        return result;
                    }
                    catch (MsalUiRequiredException ex)
                    {
                        Debug.LogError("MsalUiRequiredException");
                        Debug.LogException(ex);
                    }
                    catch (MsalServiceException ex)
                    {
                        Debug.LogError("MsalServiceException");
                        Debug.LogException(ex);
                    }
                    catch (MsalClientException ex)
                    {
                        Debug.LogError("MsalClientException");
                        Debug.LogException(ex);
                        // Mitigation: Use interactive authentication
                    }
                    catch (Exception ex)
                    {
                        Debug.LogError("Exception");
                        Debug.LogException(ex);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.LogError("GetAccountsAsync");
                Debug.LogException(ex);
            }
    
            return null;
        }
    }
    

备注

此代码并不完整,不能用于商业应用程序。This code is by no means complete and is not ready for a commercial application. 例如,你至少还可能需要添加注销功能。For example, at a minimum you'll likely want to add the ability to sign out too. 可以使用客户端应用程序提供的 Task RemoveAsync(IAccount account) 方法实现此目的。This can be done using the Task RemoveAsync(IAccount account) method provided by the client application. 此代码仅用于教程,你的实现过程将特定于你的应用程序。This code is only intended for tutorial use, your implementation will be specific to your application.

代码首先尝试使用 AquireTokenSilent 以无提示方式获取令牌。The code first tries to get the token silently using AquireTokenSilent. 如果用户之前已经对此应用程序进行了身份验证,此操作会成功。This will be successful if the user has previously authenticated this application. 如果不成功,请转到用户涉及度更高的策略。If it's not successful, move on to a more user-involved strategy.

对于此代码,我们使用设备代码流来获取访问令牌。For this code, we're using the device code flow to obtain an Access Token. 通过此流,用户可在计算机或移动设备上登录其 Azure 帐户,并将生成的令牌发送回 HoloLens 应用程序。This flow allows the user to sign in to their Azure account on a computer or mobile device and have the resulting token sent back to the HoloLens application.

从 ARR 的角度来看,此类最重要的部分是这一行:The most important part of this class from an ARR perspective is this line:

return await Task.FromResult(new SessionConfiguration(AccountDomain, AzureRemoteRenderingAccountID, "", AD_Token, ""));

在这里,我们使用帐户域、帐户 ID、帐户身份验证域和访问令牌创建新的 SessionConfiguration 对象。Here, we create a new SessionConfiguration object using the account domain, account ID, account authentication domain, and access token. 只要基于先前配置的基于角色的权限向用户授予了所需权限,ARR 服务便可以使用此令牌来查询、创建和加入远程渲染会话。This token is then used by the ARR service to query, create, and join remote rendering sessions as long as the user is authorized based on the role-based permissions configured earlier.

进行此更改后,应用程序的当前状态及其对 Azure 资源的访问权限如下所示:With this change, the current state of the application and its access to your Azure resources looks like this:

更好的安全性

由于用户凭据不存储在设备上(本例中甚至没有在设备上输入),因此其暴露风险非常低。Since the User Credentials aren't stored on the device (or in this case even entered on the device), their exposure risk is very low. 现在,设备正在使用特定于用户的、有时间限制的访问令牌访问 ARR,而 ARR 使用访问控制 (IAM) 访问 Blob 存储。Now the device is using a user-specific, time-limited Access Token to access ARR, which uses access control (IAM) to access the Blob Storage. 这两个步骤完全删除了源代码中的“密码”,从而大大提高了安全性。These two steps have completely removed the "passwords" from the source code and increased security considerably. 然而,这并不是最安全的方法,将模型和会话管理转移到 Web 服务可进一步提高安全性。However, this isn't the most security available, moving the model and session management to a web service will improve security further. 商业准备情况一章中讨论了其他安全注意事项。Additional security considerations are discussed in the Commercial Readiness chapter.

测试 AAD 身份验证Testing AAD Auth

在 Unity 编辑器中,当 AAD 身份验证处于活动状态时,需要在每次启动应用程序时进行身份验证。In the Unity Editor, when AAD Auth is active, you will need to authenticate every time you launch the application. 在设备上,首次启动时需要执行身份验证步骤,然后将仅在令牌过期或无效时才需要再次执行。On device, the authentication step will happen the first time and only be required again when the token expires or is invalidated.

  1. 将 AADAuthentication 组件添加到 RemoteRenderingCoordinator GameObject 中 。Add the AADAuthentication component to the RemoteRenderingCoordinator GameObject.

    AAD 身份验证组件

  2. 填写客户 ID 和租户 ID 的值。Fill in your values for the Client ID and the Tenant ID. 这些值可以在应用程序注册的概述页面中找到:These values can be found in your App Registration's Overview Page:

    • 帐户域与你在 RemoteRenderingCoordinator 的帐户域中使用的域相同 。Account Domain is the same domain you've been using in the RemoteRenderingCoordinator's Account Domain.
    • Active Directory 应用程序客户端 ID 是 AAD 应用注册中的应用程(客户端)ID(见下图)。Active Directory Application Client ID is the Application (client) ID found in your AAD app registration (see image below).
    • Active 租户 ID 是在 AAD 应用注册中找到的目录(租户) ID(请参阅下图)。Azure Tenant ID is the Directory (tenant) ID found in your AAD app registration ( see image below).
    • Azure 远程渲染帐户 ID 与用于 RemoteRenderingCoordinator 的帐户 ID 相同 。Azure Remote Rendering Account ID is the same Account ID you've been using for RemoteRenderingCoordinator.
    • 帐户身份验证域与你在 RemoteRenderingCoordinator 中使用的帐户身份验证域相同 。Account Authentication Domain is the same Account Authentication Domain you've been using in the RemoteRenderingCoordinator.

    屏幕截图,其中突出显示了“应用程序(客户端)ID”和“目录(租户) ID”。

  3. 在 Unity 编辑器中按“播放”并同意运行会话。Press Play in the Unity Editor and consent to running a session. 由于 AADAuthentication 组件有一个视图控制器,它将在会话授权模式面板后自动挂钩以显示提示。Since the AADAuthentication component has a view controller, its automatically hooked up to display a prompt after the session authorization modal panel.

  4. 请按照 AppMenu 右边面板中的说明操作。Follow the instructions found in the panel to the right of the AppMenu. 看到的内容应该如下所示:显示在 AppMenu 右侧显示的“指令”面板的插图。You should see something similar to this: Illustration that shows the instruction panel that appears to the right of the AppMenu. 在辅助设备(或同一设备上的浏览器)上输入提供的代码并使用凭据登录后,一个访问令牌会返回到发出请求的应用程序中(在本例中为 Unity 编辑器)。After entering the provided coded on your secondary device (or browser on the same device) and logging in using your credentials, an Access Token will be returned to the requesting application, in this case, the Unity Editor.

  5. 此后,应用程序中的所有内容应会正常运行。After this point, everything in the application should proceed normally. 如果没有按照预期的方式完成各个阶段,请检查 Unity 控制台是否有任何错误。Check the Unity Console for any errors if you're not progressing through the stages as expected.

在设备上构建Build to device

如果使用 MSAL 在设备上构建应用程序,需要在项目的 Assets 文件夹中包含一个文件。If you're building an application using MSAL to device, you'll need to include a file in your project's Assets folder. 这有助于编译器使用 Tutorial Assets 中包含的 Microsoft.Identity.Client.dll 正确地构建应用程序。This will help the compiler build the application correctly using the Microsoft.Identity.Client.dll included in the Tutorial Assets.

  1. 在 Assets 中添加名为 link.xml 的新文件 Add a new file in Assets named link.xml

  2. 将以下内容添加到文件:Add the following for to the file:

    <linker>
        <assembly fullname="Microsoft.Identity.Client" preserve="all"/>
        <assembly fullname="System.Runtime.Serialization" preserve="all"/>
        <assembly fullname="System.Core">
            <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
        </assembly>
    </linker>
    
  3. 保存更改Save the changes

遵循快速入门:将 Unity 示例部署到 HoloLens - 生成示例项目中的步骤,生成到 HoloLens。Follow the steps found in Quickstart: Deploy Unity sample to HoloLens - Build the sample Project, to build to HoloLens.

后续步骤Next steps

本教程集的其余部分包含概念性主题,用于创建使用 Azure 远程渲染的生产就绪应用程序。The remainder of this tutorial set contains conceptual topics for creating a production-ready application that uses Azure Remote Rendering.