你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

Azure IoT 客户端 SDK 对第三方令牌服务器的支持

Azure IoT
Azure IoT 中心

控制对 IoT 中心的访问一文演示了如何将第三方令牌服务与 IoT 中心集成。 本文概述了每个 Azure IoT 客户端 SDK 中对共享访问签名 (SAS) 令牌身份验证的支持。 其中还概述了需要在设备应用程序中使用每种语言的相应 SDK 实现哪些功能,以及如何将设备范围或模块范围的令牌用于 DeviceConnect 或 ModuleConnect 的共享访问策略。

上下文和问题

当前的 Azure IoT 中心安全文档讨论了 IoT 设备使用 Azure IoT 客户端 SDK 向 IoT 中心进行 SAS 身份验证时可用的第三方令牌服务器模式。 但是,客户在最近的企业互动中做出的错误假设表明,如果不进一步澄清,你可能会对 Azure IoT 客户端 SDK 中默认实现的支持级别产生误导性印象。

本文将讨论从这种互动中学到的知识,并澄清了在每个 SDK 中需要对设备执行哪些操作才能实现第三方令牌服务器身份验证。 本文还可以帮助你防止在 Azure IoT 客户端 SDK 中对第三方令牌服务器模式的支持方面做出类似的错误假设。

解决方案

Azure IoT 客户端 SDK 为 SAS 令牌身份验证提供不同级别的支持,每种支持级别需要编写一些自定义代码来完成身份验证和令牌管理功能。

令牌评估频率取决于所选的传输协议 – MQTT、AMQP 或 HTTPS。 差异取决于协议支持主动续订令牌和会话超时的能力。 只有 AMQP 实现主动续订支持。 这意味着,其他传输将在 SAS 令牌身份验证失败时关闭连接,然后需要执行新的连接操作。 对于客户端而言,这可能是一个高开销的连接操作。

如果 SAS 身份验证失败,则传输实现将引发错误,该错误可由“连接状态已更改”事件处理程序在设备应用程序中处理。 如果无法实现此类处理程序,则设备应用程序通常会因为出错而停止。 正确实现事件处理程序和令牌续订功能后,传输可以重试连接。

下图演示了第三方令牌服务器模式:

Illustration of the third-party token-server pattern

下图演示了 Azure IoT 客户端 SDK 与移动运营商的集成中的实现支持:

Flowchart of implementation support in the Azure IoT client SDK with Mobile Net Operator integration

示例实现包含在 GitHub 上的 Azure 示例存储库中。

问题和注意事项

在决定是否实现此模式时,请考虑以下几点:

参考:

何时使用此模式

每当需要使用各种 Azure IoT 客户端 SDK 从 IoT 设备向 Azure IoT 中心进行身份验证时,都应使用此模式。 不要使用客户端 SDK 进行 SAS 令牌身份验证,而应使用 Azure DPS REST API 来确保为所有传输机制实现主动续订支持。

示例

以下部分提供了可用于不同编程语言(例如 Embedded C、.NET、Java 和 Python)的示例。

适用于 C 的 Azure IoT 中心设备 SDK 和适用于 Embedded C 的 Azure IoT 中心设备 SDK

在使用 Azure IoT C SDK 或 Azure IoT Embedded C SDK 生成的设备应用程序中可利用以下方法。 这两个 SDK 都不提供 SAS 令牌生存期管理,因此你需要实现 SAS 令牌生存期管理器功能。

通过将 deviceSasToken 成员设置为令牌并将 deviceKey 设置为 null,可以通过 IOTHUB_CLIENT_CONFIG 结构使用 SAS 令牌。 其他未使用的值(例如 protocolGatewayHostName)也必须设置为 null。

IOTHUB_CLIENT_CONFIG* CONFIG = (IOTHUB_CLIENT_CONFIG*)malloc(sizeof(IOTHUB_CLIENT_CONFIG));

CONFIG->PROTOCOL = PROTOCOL;
CONFIG->DEVICEID = DEVICEID;
CONFIG->IOTHUBNAME = IOTHUBNAME;
CONFIG->IOTHUBSUFFIX = IOTHUBSUFFIX;
CONFIG->DEVICEKEY = 0;
CONFIG->DEVICESASTOKEN = TOKEN;
CONFIG->PROTOCOLGATEWAYHOSTNAME = 0;

// The created IOTHUB_CLIENT_CONFIG can then be provided to the IoTHubDeviceClient_Create function to establish a DeviceClient instance.
if ((IOTHUBCLIENTHANDLE = IoTHubDeviceClient_Create(CONFIG)) == NULL) {
    (void)printf("ERROR: IOTHUBCLIENTHANDLE IS NULL!\r\n");
}

// To capture SAS token authentication failures, a handler needs to be implemented for the IoTHubDeviceClient_SetConnectionStatusCallback.
(void)IoTHubDeviceClient_SetConnectionStatusCallback(IOTHUBCLIENTHANDLE, CONNECTION_STATUS_CALLBACK, NULL);

connection_status_callback 可以捕获 IOTHUB_CLIENT_CONNECTION_EXPIRED_SAS_TOKEN 的 IOTHUB_CLIENT_CONNECTION_STATUS_REASON 以通过第三方令牌服务触发 SAS 令牌续订。 只有这样做,所有传输才能捕获连接问题;但是,这种做法对于不支持主动 SAS 令牌续订的传输而言尤其有必要。 主动 SAS 令牌生存期管理可以实现为在设备应用程序“操作”循环期间重复运行的函数。 确保经常评估令牌的生存期,并在必要时主动执行令牌续订。

C SDK 的 SAS 令牌身份验证实现摘要:

  1. 实现 ConnectionStatusCallback 处理程序以捕获 IOTHUB_CLIENT_CONNECTION_EXPIRED_SAS_TOKEN 事件并触发令牌续订。

  2. 使用 IOTHUB_CLIENT_CONFIG 将设备 SAS 令牌提供给 IoTHubDeviceClient_Create。

  3. 实现主动 SAS 令牌生存期管理作为设备应用程序的操作循环的一部分。

适用于 .NET 的 Azure IoT 中心设备 SDK

适用于 .NET 的 Azure IoT 客户端 SDK 通过抽象 DeviceAuthenticationWithTokenRefresh 类实现 SAS 令牌生存期管理支持。 可将此类的具体实现(添加令牌续订功能)作为身份验证方法提供给 DeviceClient.Create 方法。 传输实现将根据需要通过身份验证方法自动续订令牌。 需要使用 ConnectionStatusChangesHandler 来捕获连接更改并防止传输引发异常。

基于 DeviceAuthenticationWithTokenRefreash 类的示例实现:

internal class StsDeviceAuthenticationWithTokenRefresh : DeviceAuthenticationWithTokenRefresh
{

    private readonly string _stsConnectUrl = "http://localhost:8080/sts/azure/token/operations?sr={0}/devices/{1}";

    private const int DEFAULTTIMETOLIVESECONDS = 1 * 60 * 60;

    private const int DEFAULTBUFFERPERCENTAGE = 15;

    public StsDeviceAuthenticationWithTokenRefresh(string deviceId, int suggestedTimeToLiveSeconds, int timeBufferPercentage) : BASE(deviceId, suggestedTimeToLiveSeconds, timeBufferPercentage)
    {
        If(String.IsNullOrWhitespace(deviceId)){
            throw new ArgumentNullException(nameof(deviceId));
        }
    }

    protected override async Task<string> SafeCreateNewToken(string iotHub, int suggestedTimeToLive)
    {
        string result;
        string url = string.Format(_stsConnectUrl, iotHub, deviceId);

        using (HttpClientHandler handler = new HttpClientHandler())
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                HttpResponseMessage response = await client.GetAsync(url);
                if (response.IsSuccessStatusCode)
                {
                    result = await response.Content.ReadAsStringAsync();
                }
                else
                {
                    throw new HttpRequestException($"Request failed with status code {response.StatusCode}.");
                }
            }
            catch (HttpRequestException)
            {
                result = null;
            }
        }

        return result;
    }
}

适用于 .NET 的 Azure IoT 中心设备 SDK 的 SAS 令牌身份验证实现摘要:

  1. 基于 DeviceAuthenticationWithTokenRefresh 抽象类(该类实现令牌续订功能)实现一个具体类。

  2. 实现 ConnectionStatusChangesHandler 以捕获传输连接状态并避免传输实现引发异常。

参考:

适用于 Java 的 Azure IoT 中心设备 SDK

适用于 Java 的 Azure IoT 客户端 SDK 通过 SasTokenProvider 接口实现 SAS 令牌生存期管理支持。 通过 SAS 令牌续订功能实现此接口的类可用作 DeviceClient 构造函数中的 SecurityProvider。 传输实现将根据需要通过安全提供程序自动续订令牌。 需要注册 ConnectionStatusChangeCallback 以捕获连接更改并防止传输引发异常。

实现 SasTokenProvider 接口的安全提供程序的示例实现:

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class StsSecurityProvider implements SasTokenProvider {
    private final String hostname;
    private final String deviceId;
    private int renewalBufferSeconds;
    private long expiryTimeSeconds;
    private char[] sasToken;

    public StsSecurityProvider(String hostname, String deviceId) {
        this.hostname = hostname;
        this.deviceId = deviceId;
        this.renewalBufferSeconds = 120;
        this.expiryTimeSeconds = (System.currentTimeMillis() / 1000);
    }

    @Override
    public char[] getSasToken() {
        long currentTimeSeconds = (System.currentTimeMillis() / 1000);
        try {
            if (this.sasToken == null || this.expiryTimeSeconds + this.renewalBufferSeconds >= currentTimeSeconds) {
                this.sasToken = stsGetToken();
                assert this.sasToken != null;
                String t = String.copyValueOf(this.sasToken);
                String[] bits = t.split("SE=");
                long l = Long.parseLong(bits[1]);
                this.expiryTimeSeconds = l; // the SE= number
                this.renewalBufferSeconds = (int)(l * 0.15); // renew within 15% of expiry
            }
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        }
        return this.sasToken;
    }

    private char[] stsGetToken() throws IOException, InterruptedException {
        String stsUrl = String.format("http://localhost:8080/sts/azure/token/operations?sr=%s/devices/%s", this.hostname, this.deviceId);
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(stsUrl))
            .timeout(Duration.ofMinutes(2))
            .header("Content-Type", "application/json")
            .build();
        HttpClient client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .connectTimeout(Duration.ofSeconds(20))
            .build();
        HttpResponse < String > response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            return null;
        }
        if (response.body().isEmpty()) {
            return null;
        }
        return response.body().toCharArray();
    }
}

适用于 Java 的 Azure IoT 中心设备 SDK 的 SAS 令牌身份验证实现摘要:

  1. 实现类上的 SasTokenProvider 接口并包含令牌续订功能。

  2. 实现 ConnectionStatusChangeCallback 处理程序以捕获传输连接状态更改并避免传输实现引发异常。

参考:

适用于 Python 的 Azure IoT 中心设备 SDK

适用于 Python 的 Azure IoT 中心设备 SDK 通过 IoTHubDeviceClient 对象上的方法实现 SAS 令牌支持。 使用这些方法可以通过令牌创建设备客户端,并可以在创建设备客户端后提供更新的令牌。 这些方法不实现令牌生存期管理,但令牌生存期管理可以轻松实现为异步操作。

仅显示功能概况的 Python 3.7 示例实现:

import asyncio
import iothub_device_client

async def main():
    # Get a SAS token you generated
    sastoken = get_new_sastoken()
    # The client object is used to interact with your Azure IoT Hub.
    device_client = iothub_device_client.create_from_sastoken(sastoken)

    # Connect the client
    await device_client.connect()

    # Define behavior for providing new SAS tokens to prevent expiry
    async def sastoken_keepalive():
        while True:
            await asyncio.sleep(new_token_interval)
            sastoken = get_new_sastoken()
            await device_client.update_sastoken(sastoken)

    # Also run the SAS token keepalive in the event loop
    keepalive_task = asyncio.create_task(sastoken_keepalive())

    # Cancel the SAS token update task
    keepalive_task.cancel()

    # Finally, shut down the client
    await device_client.shutdown()

if __name__ == "main":
    asyncio.run(main())

适用于 Python 的 Azure IoT 中心设备 SDK 的 SAS 令牌身份验证摘要:

  1. 创建 SAS 令牌生成函数。

  2. 使用 IoTHubDeviceClient.create_from_sastoken 创建设备客户端。

  3. 将令牌生存期作为单独的活动进行管理,并根据 IoTHubDeviceClient.update_sastoken 方法的需要为设备客户端提供续订的令牌。

参考:

适用于 Node.JS/JavaScript 的 Azure IoT 中心设备 SDK

适用于 Node.JS/JavaScript 的 Azure IoT 实现 SharedAccessSignatureAuthenticationProvider,用于向设备客户端和传输提供 SAS 令牌,以向 IoT 中心进行身份验证。 它不实现任何令牌续订功能。 设备应用程序必须管理令牌生存期,并根据需要续订令牌。

使用设备客户端方法 fromSharedAccessSignature 和 updateSharedAccessSignature 发起与 IoT 中心的连接,并向 SharedAccessSignatuteAuthenticationProvider 提供续订的令牌,这会导致身份验证提供程序向传输发出 newTokenAvailable 事件。

simple_sample_device_with_sas.js 示例中提供了一个基本的 SAS 令牌示例。

适用于 Node.JS/JavaScript 的 Azure IoT 中心设备 SDK 摘要:

  1. 实现 SAS 令牌生存期管理和续订。

  2. 使用设备客户端 fromSharedAccessSignature 构造设备客户端实例。

  3. 使用设备客户端 updateSharedAccessSignature 提供续订的令牌。

参考:

后续步骤