程式代碼逐步解說:使用 Functions 的無伺服器應用程式

Azure 事件中樞
Azure Functions

無伺服器模型會從基礎計算基礎結構中抽象化程序代碼,讓開發人員不需進行廣泛的設定,即可專注於商業規則。 無伺服器程式代碼可降低成本,因為您只需支付程式代碼執行資源和持續時間的費用。

無伺服器事件驅動模型符合特定事件觸發已定義動作的情況。 例如,接收傳入裝置訊息會觸發記憶體以供日後使用,或資料庫更新會觸發一些進一步的處理。

為了協助您探索 Azure 中的 Azure 無伺服器技術,Microsoft 開發並測試了使用 Azure Functions 的無伺服器應用程式。 本文會逐步解說無伺服器函式解決方案的程式代碼,並說明您可能會遇到的設計決策、實作詳細數據,以及一些您可能遇到的「gotchas」。

探索解決方案

兩部分解決方案描述假設的無人機遞送系統。 無人機會將飛行中狀態傳送至雲端,以儲存這些訊息以供日後使用。 Web 應用程式可讓使用者擷取訊息,以取得裝置的最新狀態。

您可以從 GitHub 下載此解決方案的程式代碼。

本逐步解說假設對下列技術有基本的熟悉度:

您不需要是 Functions 或事件中樞的專家,但您應該瞭解其在高層級的功能。 以下是開始使用的一些良好資源:

了解案例

功能區塊的圖表

Fabrikam 會管理無人機遞送服務的無人機車隊。 應用程式包含兩個主要功能區域:

  • 事件擷取。 在飛行期間,無人機會將狀態消息傳送至雲端端點。 應用程式會擷取並處理這些訊息,並將結果寫入後端資料庫 (Azure Cosmos DB)。 裝置會以 通訊協定緩衝區 (protobuf) 格式傳送訊息。 Protobuf 是一種有效率、自我描述的串行化格式。

    這些訊息包含部分更新。 在每個固定間隔內,每個無人機都會傳送包含所有狀態字段的「主要畫面格」訊息。 在主要畫面格之間,狀態消息只會包含自上次訊息後變更的欄位。 此行為通常是許多需要節省頻寬和電源的IoT裝置。

  • Web 應用程式。 Web 應用程式可讓使用者查閱裝置,並查詢裝置的最後已知狀態。 用戶必須登入應用程式,並使用 Microsoft Entra ID 進行驗證。 應用程式只允許已獲授權存取應用程式的使用者要求。

以下是 Web 應用程式的螢幕快照,其中顯示查詢的結果:

用戶端應用程式的螢幕快照

設計應用程式

Fabrikam 已決定使用 Azure Functions 來實作應用程式商業規則。 Azure Functions 是「函式即服務」(FaaS)的範例。 在此計算模型中,函式是部署至雲端並在裝載環境中執行的一段程序代碼。 此裝載環境會完全抽象化執行程式碼的伺服器。

為何選擇無伺服器方法?

具有 Functions 的無伺服器架構是事件驅動架構的範例。 函式程式代碼是由函式外部的某些事件所觸發,在此案例中為來自無人機的訊息,或用戶端應用程式的 HTTP 要求。 使用函式應用程式時,您不需要為觸發程式撰寫任何程序代碼。 您只會撰寫執行以回應觸發程式的程式代碼。 這表示您可以專注於商業規則,而不是撰寫許多程式代碼來處理基礎結構問題,例如傳訊。

使用無伺服器架構也有一些操作優點:

  • 不需要管理伺服器。
  • 計算資源會視需要動態配置。
  • 您只需支付用來執行程式碼的計算資源費用。
  • 計算資源會根據流量依需求進行調整。

架構

下圖顯示應用程式的高層級架構:

此圖顯示無伺服器 Functions 應用程式的高階架構。

在一個數據流中,箭號會顯示從裝置到事件中樞並觸發函式應用程式的訊息。 從應用程式,一個箭號會顯示傳送至記憶體佇列的寄不出的信件訊息,另一個箭號則會顯示寫入 Azure Cosmos DB。 在另一個數據流中,箭號會顯示用戶端 Web 應用程式透過 CDN 從 Blob 記憶體靜態 Web 裝載取得靜態檔案。 另一個箭號顯示經過 API 管理 的用戶端 HTTP 要求。 從 API 管理,一個箭號會顯示從 Azure Cosmos DB 觸發和讀取數據的函式應用程式。 另一個箭號會顯示透過 Microsoft Entra 識別碼的驗證。 使用者也會登入 Microsoft Entra ID。

事件擷取:

  1. 無人機訊息會由 Azure 事件中樞 內嵌。
  2. 事件中樞會產生包含訊息數據的事件數據流。
  3. 這些事件會觸發 Azure Functions 應用程式來處理它們。
  4. 結果會儲存在 Azure Cosmos DB 中。

Web 應用程式:

  1. 靜態檔案是由來自 Blob 記憶體的 CDN 提供。
  2. 使用者使用 Microsoft Entra ID 登入 Web 應用程式。
  3. Azure API 管理 可作為公開 REST API 端點的閘道。
  4. 來自客戶端的 HTTP 要求會觸發從 Azure Cosmos DB 讀取並傳回結果的 Azure Functions 應用程式。

此應用程式是以兩個參考架構為基礎,對應至上述兩個功能區塊:

您可以閱讀這些文章,以深入瞭解高階架構、解決方案中使用的 Azure 服務,以及延展性、安全性和可靠性的考慮。

無人機遙測功能

讓我們從查看處理來自事件中樞無人機訊息的函式開始。 函式定義於名為 RawTelemetryFunction的類別中:

namespace DroneTelemetryFunctionApp
{
    public class RawTelemetryFunction
    {
        private readonly ITelemetryProcessor telemetryProcessor;
        private readonly IStateChangeProcessor stateChangeProcessor;
        private readonly TelemetryClient telemetryClient;

        public RawTelemetryFunction(ITelemetryProcessor telemetryProcessor, IStateChangeProcessor stateChangeProcessor, TelemetryClient telemetryClient)
        {
            this.telemetryProcessor = telemetryProcessor;
            this.stateChangeProcessor = stateChangeProcessor;
            this.telemetryClient = telemetryClient;
        }
    }
    ...
}

這個類別有數個相依性,這些相依性會使用相依性插入插入建構函式:

  • ITelemetryProcessorIStateChangeProcessor 介面會定義兩個協助程序物件。 如我們所見,這些物件會執行大部分的工作。

  • TelemetryClient 是 Application Insights SDK 的一部分。 它用來將自定義應用程式計量傳送至 Application Insights。

稍後,我們將探討如何設定相依性插入。 現在,只要假設這些相依性存在。

設定事件中樞觸發程式

函式中的邏輯會實作為名為的 RunAsync異步方法。 以下是方法簽章:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    // implementation goes here
}

方法會採用下列參數:

  • messages 是事件中樞訊息的陣列。
  • deadLetterMessages是用來儲存寄不出的信件訊息的 Azure 儲存體 佇列。
  • logging 提供記錄介面,用於寫入應用程式記錄。 這些記錄會傳送至 Azure 監視器。

參數 EventHubTrigger 上的 messages 屬性會設定觸發程式。 屬性的屬性會指定事件中樞名稱、連接字串 和取用者群組。 (取 用者群組 是事件中樞事件數據流的隔離檢視。這個抽象概念允許相同事件中樞的多個取用者。

請注意某些屬性屬性中的百分比符號 \ 。 這些表示 屬性會指定應用程式設定的名稱,而實際值則是在運行時間從該應用程式設定中取得。 否則,如果沒有百分比符號,屬性就會提供常值。

屬性 Connection 是例外狀況。 這個屬性一律會指定應用程式設定名稱,絕不是常值,因此不需要百分比符號。 此區別的原因是 連接字串 是秘密,而且不應該簽入原始程式碼。

雖然其他兩個屬性(事件中樞名稱和取用者群組)不是像 連接字串 這樣的敏感數據,但最好將它們放入應用程式設定中,而不是硬式編碼。 如此一來,即可更新它們,而不需要重新編譯應用程式。

如需設定此觸發程式的詳細資訊,請參閱 azure Functions 的 Azure 事件中樞 系結。

訊息處理邏輯

以下是處理一批訊息的方法實作 RawTelemetryFunction.RunAsync

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    telemetryClient.GetMetric("EventHubMessageBatchSize").TrackValue(messages.Length);

    foreach (var message in messages)
    {
        DeviceState deviceState = null;

        try
        {
            deviceState = telemetryProcessor.Deserialize(message.Body.Array, logger);

            try
            {
                await stateChangeProcessor.UpdateState(deviceState, logger);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error updating status document", deviceState);
                await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message, DeviceState = deviceState });
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
            await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
        }
    }
}

叫用函式時, messages 參數會包含來自事件中樞的訊息數位。 在批次中處理訊息通常會比一次讀取一則訊息產生更好的效能。 不過,您必須確定函式具有復原性,並正常處理失敗和例外狀況。 否則,如果函式在批次中間擲回未處理的例外狀況,您可能會遺失其餘的訊息。 在錯誤處理一節中會更詳細地討論此考慮。

但是,如果您忽略例外狀況處理,則每個訊息的處理邏輯都很簡單:

  1. 呼叫 ITelemetryProcessor.Deserialize 以還原串行化包含裝置狀態變更的訊息。
  2. 呼叫 IStateChangeProcessor.UpdateState 以處理狀態變更。

讓我們更詳細地看看這兩種方法,從 Deserialize 方法開始。

還原串行化方法

方法 TelemetryProcess.Deserialize 會採用包含訊息承載的位元組數位。 它會還原串行化這個承載,並傳 DeviceState 回 物件,此物件代表無人機的狀態。 狀態可能代表部分更新,只包含來自上次已知狀態的差異。 因此,方法必須處理 null 還原串行化承載中的欄位。

public class TelemetryProcessor : ITelemetryProcessor
{
    private readonly ITelemetrySerializer<DroneState> serializer;

    public TelemetryProcessor(ITelemetrySerializer<DroneState> serializer)
    {
        this.serializer = serializer;
    }

    public DeviceState Deserialize(byte[] payload, ILogger log)
    {
        DroneState restored = serializer.Deserialize(payload);

        log.LogInformation("Deserialize message for device ID {DeviceId}", restored.DeviceId);

        var deviceState = new DeviceState();
        deviceState.DeviceId = restored.DeviceId;

        if (restored.Battery != null)
        {
            deviceState.Battery = restored.Battery;
        }
        if (restored.FlightMode != null)
        {
            deviceState.FlightMode = (int)restored.FlightMode;
        }
        if (restored.Position != null)
        {
            deviceState.Latitude = restored.Position.Value.Latitude;
            deviceState.Longitude = restored.Position.Value.Longitude;
            deviceState.Altitude = restored.Position.Value.Altitude;
        }
        if (restored.Health != null)
        {
            deviceState.AccelerometerOK = restored.Health.Value.AccelerometerOK;
            deviceState.GyrometerOK = restored.Health.Value.GyrometerOK;
            deviceState.MagnetometerOK = restored.Health.Value.MagnetometerOK;
        }
        return deviceState;
    }
}

這個方法會使用另一個協助程式介面 ITelemetrySerializer<T>,將原始訊息還原串行化。 然後,結果會轉換成 更容易使用的POCO 模型。 此設計有助於隔離處理邏輯與串行化實作詳細數據。 介面 ITelemetrySerializer<T> 定義於共用連結庫中,裝置模擬器也會使用此連結庫來產生模擬裝置事件,並將其傳送至事件中樞。

using System;

namespace Serverless.Serialization
{
    public interface ITelemetrySerializer<T>
    {
        T Deserialize(byte[] message);

        ArraySegment<byte> Serialize(T message);
    }
}

UpdateState 方法

方法 StateChangeProcessor.UpdateState 會套用狀態變更。 每個無人機的最後已知狀態會儲存為 Azure Cosmos DB 中的 JSON 檔。 由於無人機會傳送部分更新,因此當應用程式取得更新時,無法直接覆寫檔。 相反地,它必須擷取先前的狀態、合併欄位,然後執行 upsert 作業。

public class StateChangeProcessor : IStateChangeProcessor
{
    private IDocumentClient client;
    private readonly string cosmosDBDatabase;
    private readonly string cosmosDBCollection;

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    {
        this.client = client;
        this.cosmosDBDatabase = options.Value.COSMOSDB_DATABASE_NAME;
        this.cosmosDBCollection = options.Value.COSMOSDB_DATABASE_COL;
    }

    public async Task<ResourceResponse<Document>> UpdateState(DeviceState source, ILogger log)
    {
        log.LogInformation("Processing change message for device ID {DeviceId}", source.DeviceId);

        DeviceState target = null;

        try
        {
            var response = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(cosmosDBDatabase, cosmosDBCollection, source.DeviceId),
                                                            new RequestOptions { PartitionKey = new PartitionKey(source.DeviceId) });

            target = (DeviceState)(dynamic)response.Resource;

            // Merge properties
            target.Battery = source.Battery ?? target.Battery;
            target.FlightMode = source.FlightMode ?? target.FlightMode;
            target.Latitude = source.Latitude ?? target.Latitude;
            target.Longitude = source.Longitude ?? target.Longitude;
            target.Altitude = source.Altitude ?? target.Altitude;
            target.AccelerometerOK = source.AccelerometerOK ?? target.AccelerometerOK;
            target.GyrometerOK = source.GyrometerOK ?? target.GyrometerOK;
            target.MagnetometerOK = source.MagnetometerOK ?? target.MagnetometerOK;
        }
        catch (DocumentClientException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                target = source;
            }
        }

        var collectionLink = UriFactory.CreateDocumentCollectionUri(cosmosDBDatabase, cosmosDBCollection);
        return await client.UpsertDocumentAsync(collectionLink, target);
    }
}

此程式代碼會 IDocumentClient 使用 介面從 Azure Cosmos DB 擷取檔。 如果檔存在,新的狀態值會合併至現有的檔。 否則會建立新的文件。 這兩個案例都是由 UpsertDocumentAsync 方法處理。

此程式代碼已針對檔已經存在且可合併的情況進行優化。 在指定無人機的第一個遙測訊息上 ReadDocumentAsync ,方法會擲回例外狀況,因為該無人機沒有檔。 第一則訊息之後,檔將可供使用。

請注意,這個類別會使用相依性插入來插入 IDocumentClient Azure Cosmos DB 的 ,以及 IOptions<T> 具有組態設定的 。 我們稍後將瞭解如何設定相依性插入。

注意

Azure Functions 支援 Azure Cosmos DB 的輸出系結。 此系結可讓函式應用程式在 Azure Cosmos DB 中撰寫檔,而不需要任何程式代碼。 不過,輸出系結不適用於此特定案例,因為需要自定義 upsert 邏輯。

錯誤處理

如先前所述,函 RawTelemetryFunction 式應用程式會在迴圈中處理一批訊息。 這表示函式必須正常處理任何例外狀況,並繼續處理批次的其餘部分。 否則,可能會卸除訊息。

如果在處理訊息時遇到例外狀況,函式會將訊息放入寄不出的信件佇列:

catch (Exception ex)
{
    logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
    await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
}

寄不出的信件佇列是使用 記憶體佇列的輸出系結 來定義:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]  // App setting that holds the connection string
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,  // output binding
    ILogger logger)

在這裡,Queue屬性會指定輸出系結,而 StorageAccount 屬性會指定保留記憶體帳戶 連接字串 的應用程式設定名稱。

部署提示:在建立記憶體帳戶的 Resource Manager 範本中,您可以使用 連接字串 自動填入應用程式設定。 訣竅是使用 listKeys 函 式。

以下是為佇列建立記憶體帳戶的範本區段:

    {
        "name": "[variables('droneTelemetryDeadLetterStorageQueueAccountName')]",
        "type": "Microsoft.Storage/storageAccounts",
        "location": "[resourceGroup().location]",
        "apiVersion": "2017-10-01",
        "sku": {
            "name": "[parameters('storageAccountType')]"
        },

以下是建立函式應用程式的範本區段。


    {
        "apiVersion": "2015-08-01",
        "type": "Microsoft.Web/sites",
        "name": "[variables('droneTelemetryFunctionAppName')]",
        "location": "[resourceGroup().location]",
        "tags": {
            "displayName": "Drone Telemetry Function App"
        },
        "kind": "functionapp",
        "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            ...
        ],
        "properties": {
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            "siteConfig": {
                "appSettings": [
                    {
                        "name": "DeadLetterStorage",
                        "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('droneTelemetryDeadLetterStorageQueueAccountName'), ';AccountKey=', listKeys(variables('droneTelemetryDeadLetterStorageQueueAccountId'),'2015-05-01-preview').key1)]"
                    },
                    ...

這會定義名為 DeadLetterStorage 的應用程式設定,其值會使用 函 listKeys 式填入。 請務必讓函式應用程式資源相依於記憶體帳戶資源(請參閱 dependsOn 元素)。 這可確保會先建立記憶體帳戶,且 連接字串 可供使用。

設定相依性插入

下列程式代碼會設定函式的 RawTelemetryFunction 相依性插入:

[assembly: FunctionsStartup(typeof(DroneTelemetryFunctionApp.Startup))]

namespace DroneTelemetryFunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddOptions<StateChangeProcessorOptions>()
                .Configure<IConfiguration>((configSection, configuration) =>
                {
                    configuration.Bind(configSection);
                });

            builder.Services.AddTransient<ITelemetrySerializer<DroneState>, TelemetrySerializer<DroneState>>();
            builder.Services.AddTransient<ITelemetryProcessor, TelemetryProcessor>();
            builder.Services.AddTransient<IStateChangeProcessor, StateChangeProcessor>();

            builder.Services.AddSingleton<IDocumentClient>(ctx => {
                var config = ctx.GetService<IConfiguration>();
                var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
                var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
                return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
            });
        }
    }
}

針對 .NET 撰寫的 Azure Functions 可以使用 ASP.NET Core 相依性插入架構。 基本概念是宣告元件的啟動方法。 方法會採用 IFunctionsHostBuilder 介面,用來宣告 DI 的相依性。 您可以在物件上Services呼叫 Add* 方法來執行此動作。 當您新增相依性時,您會指定其存留期:

  • 每次要求暫時性 物件時都會建立。
  • 每個函式執行會建立範圍物件一次。
  • 單一物件會在函式主機的存留期內跨函式執行重複使用。

在此範例中 TelemetryProcessor ,和 StateChangeProcessor 物件會宣告為暫時性。 這適用於輕量型無狀態服務。 另一方面,類別 DocumentClient 應該是單一類別,以獲得最佳效能。 如需詳細資訊,請參閱 Azure Cosmos DB 和 .NET 的效能秘訣。

如果您回到 RawTelemetryFunction 的程式代碼,您會看到另一個相依性不會出現在 DI 設定程式碼中,也就是TelemetryClient用來記錄應用程式計量的類別。 Functions 運行時間會自動將此類別註冊到 DI 容器,因此您不需要明確地註冊它。

如需 Azure Functions 中 DI 的詳細資訊,請參閱下列文章:

在 DI 中傳遞組態設定

有時候必須使用某些組態值初始化 物件。 一般而言,這些設定應該來自應用程式設定,或 Azure 金鑰保存庫 的秘密。

此應用程式中有兩個範例。 首先,類別 DocumentClient 會採用 Azure Cosmos DB 服務端點和密鑰。 針對這個物件,應用程式會註冊將由 DI 容器叫用的 Lambda。 此 Lambda 會 IConfiguration 使用 介面來讀取組態值:

builder.Services.AddSingleton<IDocumentClient>(ctx => {
    var config = ctx.GetService<IConfiguration>();
    var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
    var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
    return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
});

第二個範例是 類別 StateChangeProcessor 。 針對這個對象,我們使用稱為 選項模式的方法。 以下說明其運作方式:

  1. 定義包含組態設定的類別 T 。 在此情況下,Azure Cosmos DB 資料庫名稱和集合名稱。

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. 將類別 T 新增為 DI 的選項類別。

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. 在所設定類別的建構函式中,包含 IOptions<T> 參數。

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    

DI 系統會使用組態值自動填入 options 類別,並將此值傳遞至建構函式。

此方法有數個優點:

  • 將類別與組態值的來源分離。
  • 輕鬆設定不同的組態來源,例如環境變數或 JSON 組態檔。
  • 簡化單元測試。
  • 使用強型別選項類別,其容易出錯,而不只是傳入純量值。

GetStatus 函式

此解決方案中的其他 Functions 應用程式會實作簡單的 REST API,以取得無人機的最後已知狀態。 此函式定義於名為 GetStatusFunction的類別中。 以下是函式的完整程式代碼:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;

namespace DroneStatusFunctionApp
{
    public static class GetStatusFunction
    {
        public const string GetDeviceStatusRoleName = "GetStatus";

        [FunctionName("GetStatusFunction")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req,
            [CosmosDB(
                databaseName: "%COSMOSDB_DATABASE_NAME%",
                collectionName: "%COSMOSDB_DATABASE_COL%",
                ConnectionStringSetting = "COSMOSDB_CONNECTION_STRING",
                Id = "{Query.deviceId}",
                PartitionKey = "{Query.deviceId}")] dynamic deviceStatus,
            ClaimsPrincipal principal,
            ILogger log)
        {
            log.LogInformation("Processing GetStatus request.");

            if (!principal.IsAuthorizedByRoles(new[] { GetDeviceStatusRoleName }, log))
            {
                return new UnauthorizedResult();
            }

            string deviceId = req.Query["deviceId"];
            if (deviceId == null)
            {
                return new BadRequestObjectResult("Missing DeviceId");
            }

            if (deviceStatus == null)
            {
                return new NotFoundResult();
            }
            else
            {
                return new OkObjectResult(deviceStatus);
            }
        }
    }
}

此函式會使用 HTTP 觸發程式來處理 HTTP GET 要求。 函式會使用 Azure Cosmos DB 輸入系結來擷取要求的檔。 其中一個考慮是,此系結會在函式內執行授權邏輯之前執行。 如果未經授權的使用者要求檔,函式系結仍會擷取檔。 然後授權碼會傳回 401,因此使用者不會看到檔。 此行為是否可接受,可能取決於您的需求。 例如,這種方法可能會讓稽核敏感數據的數據存取更加困難。

驗證與授權

Web 應用程式會使用 Microsoft Entra 識別碼來驗證使用者。 由於應用程式是瀏覽器中 執行的單頁應用程式 (SPA),因此隱含授與流程 是適當的:

  1. Web 應用程式會將使用者重新導向至識別提供者(在此案例中為 Microsoft Entra ID)。
  2. 使用者輸入其認證。
  3. 識別提供者會使用存取令牌重新導向回 Web 應用程式。
  4. Web 應用程式會將要求傳送至 Web API,並在授權標頭中包含存取令牌。

隱含流程圖

函式應用程式可以設定為使用零個程式代碼來驗證使用者。 如需詳細資訊,請參閱 Azure App Service 中的驗證與授權

另一方面,授權通常需要一些商業規則。 Microsoft Entra ID 支援 宣告型驗證。 在此模型中,使用者的身分識別會以一組來自識別提供者的宣告表示。 宣告可以是使用者的任何資訊片段,例如其名稱或電子郵件位址。

存取令牌包含使用者宣告的子集。 其中包括使用者指派給的任何應用程式角色。

principal 式的參數是 ClaimsPrincipal 物件,其中包含來自存取令牌的宣告。 每個宣告都是宣告類型和宣告值的索引鍵/值組。 應用程式會使用這些來授權要求。

下列擴充方法會測試物件是否 ClaimsPrincipal 包含一組角色。 如果遺漏任何指定的角色,則會傳 false 回 。 如果此方法傳回 false,則函式會傳回 HTTP 401 (未經授權)。

namespace DroneStatusFunctionApp
{
    public static class ClaimsPrincipalAuthorizationExtensions
    {
        public static bool IsAuthorizedByRoles(
            this ClaimsPrincipal principal,
            string[] roles,
            ILogger log)
        {
            var principalRoles = new HashSet<string>(principal.Claims.Where(kvp => kvp.Type == "roles").Select(kvp => kvp.Value));
            var missingRoles = roles.Where(r => !principalRoles.Contains(r)).ToArray();
            if (missingRoles.Length > 0)
            {
                log.LogWarning("The principal does not have the required {roles}", string.Join(", ", missingRoles));
                return false;
            }

            return true;
        }
    }
}

如需此應用程式中驗證和授權的詳細資訊,請參閱 參考架構的安全性考慮 一節。

下一步

一旦您瞭解此參考解決方案的運作方式,請瞭解類似解決方案的最佳做法和建議。

  • 如需無伺服器事件擷取解決方案,請參閱 使用 Azure Functions 的無伺服器事件處理。
  • 如需無伺服器 Web 應用程式,請參閱 Azure 上的無伺服器 Web 應用程式。

Azure Functions 只是一個 Azure 計算選項。 如需選擇計算技術的協助,請參閱 為您的應用程式選擇 Azure 計算服務。