亚马逊AWS官方博客

如何把设备安全的接入AWS IoT(二)

1.简介

本系列文章前面一篇介绍了 AWS IoT 支持的所有协议,认证和授权方式的原理和细节,以及其分别所适应的场景。并介绍了设备使用 IAM 身份认证接入 AWS IoT 的参考步骤和参考代码。接下来本文介绍设备使用 Amazon Cognito 身份认证方式接入 AWS IoT 的参考步骤和参考代码。

在进行下一步之前,请确保已执行完本系列文档第一篇的第4章,准备工作。

2. 设备使用 Amazon Cognito 身份认证方式接入

您可以使用第三方的身份,如 Google,Facebook,OIDC,SAML 等,或者用户开发的自定义的身份,在 Cognito 身份池中交换得到 Cognito 身份,然后使用 Cognito 身份来认证设备。Cognito 身份的鉴权方式则比较复杂,首先 Cognito 身份池会为经过认证的身份配置一个 Role,进而使用 Role 的 IAM Policy 对请求进行鉴权,另外,Cognito 身份也会在 AWS IoT 中绑定一个 IoT Policy,这个 IoT Policy 也会对请求进行鉴权。实际上,Cognito 身份的最终权限是身份池 Role的IAM policy 与 Cognito 身份的 IoT Policy 的交集。Cognito 身份可以用于 mobile 场景的移动端认证。

注:由于 IoT Policy 支持 IoT 上下文的多种策略变量,通常建议的做法是,在 IAM Policy给一个比较大的权限,然后在 IoT Policy 中通过策略变量来实现精细化的权限管理。

设备使用 Amazon Cognito 身份认证的流程示意图:

2.1 创建 Amazon Cognito 身份池和相关权限

首先,创建 Cognito 身份池

IdentityPoolId=`aws cognito-identity create-identity-pool \
--identity-pool-name IoTDevicesPool \
--no-allow-unauthenticated-identities \
--developer-provider-name login.IoTDemo.dev | jq .IdentityPoolId| sed 's/"//g'`

把 IdentityPoolId 配置到环境变量中。

echo "export IdentityPoolId=$IdentityPoolId" >> ~/.bashrc

创建经过认证的 Cognito 身份代入的角色。

IoTDeviceRoleInCognitoArn=`aws iam create-role \
--role-name IoTDeviceRoleInCognito --assume-role-policy-document "{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {
      \"Effect\": \"Allow\",
      \"Principal\": {
        \"Federated\": \"cognito-identity.amazonaws.com\"
      },
      \"Action\": \"sts:AssumeRoleWithWebIdentity\",
      \"Condition\": {
        \"StringEquals\": {
          \"cognito-identity.amazonaws.com:aud\": \"${IdentityPoolId}\"
        },
        \"ForAnyValue:StringLike\": {
          \"cognito-identity.amazonaws.com:amr\": \"authenticated\"
        }
      }
    }
  ]
}" | jq .Role.Arn | sed 's/"//g'`

绑定角色到 Cognito 身份池

aws cognito-identity set-identity-pool-roles \
--identity-pool-id ${IdentityPoolId} \
--roles authenticated=${IoTDeviceRoleInCognitoArn}

给角色绑定可以 Attach IoT Policy 的权限

设备在使用 Cognito 身份接入 IoT 的时候,还需要为其绑定一个 IoT Policy。 通常情况下,考虑权限的安全,这一步需要在后端的服务来执行,这里为了简化,由设备来为自己绑定 IoT Policy,生产系统中应该禁止使用这种方法。

IoTPolicyManagerArn=`aws iam create-policy \
--policy-name IoTPolicyManager \
--policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
        {
            \"Sid\": \"VisualEditor0\",
            \"Effect\": \"Allow\",
            \"Action\": \"iot:AttachPolicy\",
            \"Resource\": \"*\"
        }
    ]
}" | jq .Policy.Arn| sed 's/"//g'`

把 IAM Policy 绑定到角色。

aws iam attach-role-policy --role-name IoTDeviceRoleInCognito \
--policy-arn ${IoTPolicyManagerArn}

Cognito 身份池支持多种身份认证的方式,如 Google,Facebook,Amazon,OID C等,这里使用了开发人员验证的身份来获取身份。

2.2 创建并部署开发人员验证的身份程序

创建一个 IAM 用户,developerIdpUser。这个用户用来给 developer provider 程序提供权限。

developer provider 程序的作用是给设备分配身份,并且调用 Cognito 的 GetOpenIdTokenForDeveloperIdentity 接口返回 Cognito 身份池的身份和令牌。

aws iam create-user --user-name developerIdpUser

创建策略,并把策略绑定到 developerIdpUser 用户

developerIdpPolicy_arn=`aws iam create-policy \
--policy-name developerIdpPolicy \
--policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
        {
        \"Sid\": \"VisualEditor0\",
        \"Effect\": \"Allow\",
        \"Action\": \"cognito-identity:GetOpenIdTokenForDeveloperIdentity\",
        \"Resource\": \"arn:aws-cn:cognito-identity:cn-north-1:${account_id}:identitypool/${IdentityPoolId}\"
        }
    ]
}" | jq .Policy.Arn | sed 's/"//g'`

aws iam attach-user-policy --user-name developerIdpUser \
--policy-arn ${developerIdpPolicy_arn}

为 developerIdpUser 用户创建 Access Key

aws iam create-access-key \
    --user-name developerIdpUser > /tmp/IoT_demo_access_key2

记录下 AccessKeyId 和 SecretAccessKey

AccessKeyId=`cat /tmp/IoT_demo_access_key2 | jq .AccessKey.AccessKeyId| sed 's/"//g'`
SecretAccessKey=`cat /tmp/IoT_demo_access_key2 | jq .AccessKey.SecretAccessKey| sed 's/"//g'`

生成 developer provider 程序。

cat <<EOF > ~/awsIoTAccessDemo/developer_provider.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import boto3
import argparse
import json
from flask import Flask, abort, request, jsonify
#获取参数

parser = argparse.ArgumentParser(description='developer idp provider.')
parser.add_argument('--developer_provider_name', default='login.IoTDemo.dev',
            help='developer_provider_name')
parser.add_argument('--identityPoolId', required=True,
            help='identityPoolId')
parser.add_argument('--AccessKeyId', required=True,
            help='AccessKeyId')
parser.add_argument('--SecretAccessKey', required=True,
            help='SecretAccessKey')

args = parser.parse_args()
developer_provider_name = args.developer_provider_name
identityPoolId = args.identityPoolId
access_key_id = args.AccessKeyId
secret_access_key = args.SecretAccessKey

app = Flask(__name__)

region = 'cn-north-1'
tokenDuration = 3600

cognito_client = boto3.client('cognito-identity',region_name=region,aws_access_key_id=access_key_id,aws_secret_access_key=secret_access_key)
#cognito_client = boto3.client('cognito-identity',region_name=region)

@app.route('/login/', methods=['POST'])
def login():
    if not request.json or 'device_id' not in request.json:
        abort(400)

    device_id = request.json['device_id']

    try:
        response = cognito_client.get_open_id_token_for_developer_identity(IdentityPoolId=identityPoolId,Logins={developer_provider_name:device_id},TokenDuration=tokenDuration)
        token = response['Token']
        identityId = response['IdentityId']
        return jsonify({'result': 'success','identityId':identityId,'token':token})
    except Exception as e:
        return jsonify({'result': 'failed'})

if __name__ == "__main__":
# 将host设置为0.0.0.0,则外网用户也可以访问到这个服务

 app.run(host="0.0.0.0", port=8383, debug=True)
EOF

运行 developer_provider.py

python developer_provider.py --identityPoolId ${IdentityPoolId} \
--AccessKeyId ${AccessKeyId} --SecretAccessKey ${SecretAccessKey}

developer_provider.py 在 http://0.0.0.0:8383/  接受请求,并返回 Cognito 身份池的身份信息。

打开一个新的 SSH 窗口,定位到 awsIoTAccessDemo 目录。

cd ~/awsIoTAccessDemo

2.3 设备使用 HTTP 协议接入

通过 Cognito 方式认证的设备,需要 IAM Policy 和 IoT Policy 同时授权。

为设备创建 IAM Policy,并将其绑定到 Cognito 身份池的角色。

IoTDeviceCognitoHttpPolicyArn=`aws iam create-policy \
--policy-name IoTDeviceCognitoHttpPolicy \
--policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
        {
            \"Sid\": \"VisualEditor0\",
            \"Effect\": \"Allow\",
            \"Action\": \"iot:Publish\",
            \"Resource\": [
                \"arn:aws-cn:iot:cn-north-1:${account_id}:topic/IoTDemo/device_cognito_http\"
            ]
        }
    ]
}" | jq .Policy.Arn | sed 's/"//g'`

aws iam attach-role-policy --role-name IoTDeviceRoleInCognito \
--policy-arn ${IoTDeviceCognitoHttpPolicyArn}

创建 IoT Policy

aws iot create-policy --policy-name IoTPolicyForDeviceCognitohttp \
 --policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
        {
        \"Effect\": \"Allow\",
        \"Action\": \"iot:Publish\",
        \"Resource\": [
                \"arn:aws-cn:iot:cn-north-1:${account_id}:topic/IoTDemo/device_cognito_http\"
            ]
        }
    ]
}"

生成设备模拟程序。

cat <<EOF > ~/awsIoTAccessDemo/device_cognito_http.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import boto3
import argparse
import json
import requests
#获取参数

parser = argparse.ArgumentParser(description='Send data to IoT Core')
parser.add_argument('--data', default="data from device_cognito_http",
            help='data to IoT')
parser.add_argument('--iot_policy_name', default="IoTPolicyForDeviceCognitohttp",
            help='iot policy name for device cognito http.')
parser.add_argument('--developer_provicer_endpoint', default="http://127.0.0.1:8383/login/",
            help='developer ID provider endpoint')

args = parser.parse_args()
data = args.data
policy_name = args.iot_policy_name
developer_provicer_endpoint = args.developer_provicer_endpoint

device_name = 'device_cognito_http'
region = 'cn-north-1'
topic = "IoTDemo/"+device_name

headers = {"Content-Type": "application/json"}
body = {"device_id":device_name}
r1 = requests.post(developer_provicer_endpoint, data=json.dumps(body), headers = headers)

token = json.loads(r1.text)['token']
identityId = json.loads(r1.text)['identityId']

cognito_client = boto3.client('cognito-identity', region_name=region)
response = cognito_client.get_credentials_for_identity(IdentityId=identityId, Logins={'cognito-identity---cn-north-1.amazonaws.com.rproxy.goskope.com.cn':token})

sessionToken=response['Credentials']['SessionToken']
accessKeyId=response['Credentials']['AccessKeyId']
secretKey=response['Credentials']['SecretKey']

iot_client = boto3.client('iot',region_name=region,aws_access_key_id=accessKeyId,aws_secret_access_key=secretKey,aws_session_token=sessionToken)

response = iot_client.attach_policy(
    policyName=policy_name,
    target=identityId
)

iot_data_client = boto3.client('iot-data',region_name=region,aws_access_key_id=accessKeyId,aws_secret_access_key=secretKey,aws_session_token=sessionToken)

response = iot_data_client.publish(
    topic=topic,
    qos=0,
    payload=json.dumps({"source":device_name, "data":data})
)

EOF

运行设备模拟程序发送消息。

python device_cognito_http.py --data "data from device cognito http." \
--developer_provicer_endpoint "http://127.0.0.1:8383/login/" \
--iot_policy_name IoTPolicyForDeviceCognitohttp

在本系列文章第一篇的第4.3章节打开的控制台中查看 AWS IoT 收到的消息。

2.4 设备使用 MQTT OVER WEBSOCKET 协议接入

通过 Cognito 方式认证的设备,需要 IAM Policy 和 IoT Policy 同时授权。

为设备创建 IAM Policy,并将其绑定到 Cognito 角色。

IoTDeviceCognitoWebsocketPolicyArn=`aws iam create-policy \
--policy-name IoTDeviceCognitoWebsocketPolicy \
--policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
        {
            \"Sid\": \"VisualEditor0\",
            \"Effect\": \"Allow\",
            \"Action\": [
                \"iot:Publish\",
                \"iot:Receive\"
            ],
            \"Resource\": \"arn:aws-cn:iot:cn-north-1:${account_id}:topic/IoTDemo/device_cognito_websocket\"
        },
        {
            \"Sid\": \"VisualEditor1\",
            \"Effect\": \"Allow\",
            \"Action\": \"iot:Connect\",
            \"Resource\": \"arn:aws-cn:iot:cn-north-1:${account_id}:client/device_cognito_websocket\"
        },
        {
            \"Sid\": \"VisualEditor2\",
            \"Effect\": \"Allow\",
            \"Action\": \"iot:Subscribe\",
            \"Resource\": \"arn:aws-cn:iot:cn-north-1:${account_id}:topicfilter/IoTDemo/device_cognito_websocket\"
        }
    ]
}" | jq .Policy.Arn | sed 's/"//g'`

aws iam attach-role-policy --role-name IoTDeviceRoleInCognito \
--policy-arn ${IoTDeviceCognitoWebsocketPolicyArn}

创建 IoT Policy。

aws iot create-policy --policy-name IoTPolicyForDeviceCognitoWebsocket \
--policy-document "{
\"Version\": \"2012-10-17\",
    \"Statement\": [
        {
            \"Sid\": \"VisualEditor0\",
            \"Effect\": \"Allow\",
            \"Action\": [
                \"iot:Publish\",
                \"iot:Receive\"
            ],
            \"Resource\": \"arn:aws-cn:iot:cn-north-1:${account_id}:topic/IoTDemo/device_cognito_websocket\"
        },
        {
            \"Sid\": \"VisualEditor1\",
            \"Effect\": \"Allow\",
            \"Action\": \"iot:Connect\",
            \"Resource\": \"arn:aws-cn:iot:cn-north-1:${account_id}:client/device_cognito_websocket\"
        },
        {
            \"Sid\": \"VisualEditor2\",
            \"Effect\": \"Allow\",
            \"Action\": \"iot:Subscribe\",
            \"Resource\": \"arn:aws-cn:iot:cn-north-1:${account_id}:topicfilter/IoTDemo/device_cognito_websocket\"
        }
    ]
}" 

生成设备模拟程序。

cat <<EOF > ~/awsIoTAccessDemo/device_cognito_websocket.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import json
import time
import sys
import boto3
import requests
import argparse
#获取参数

parser = argparse.ArgumentParser(description='Send data to IoT Core')
parser.add_argument('--iot_policy_name', default="IoTPolicyForDeviceCognitoWebsocket",
            help='iot policy name for device_cognito_websocket.')
parser.add_argument('--developer_provicer_endpoint', default="http://127.0.0.1:8383/login/",
            help='developer ID provider endpoint')
parser.add_argument('--endpoint_prefix', required=True,
            help='your iot endpoint prefix.')

args = parser.parse_args()
policy_name = args.iot_policy_name
developer_provicer_endpoint = args.developer_provicer_endpoint
endpoint_prefix = args.endpoint_prefix
device_name = 'device_cognito_websocket'
region = 'cn-north-1'
private_topic = "IoTDemo/"+device_name
server_root_ca_file = "./AmazonRootCA1.pem"
endpoint = "%s---ats---iot---cn-north-1.amazonaws.com.rproxy.goskope.com.cn" % endpoint_prefix
port = 443

headers = {"Content-Type": "application/json"}
body = {"device_id":device_name}
r1 = requests.post(developer_provicer_endpoint, data=json.dumps(body), headers = headers)
token = json.loads(r1.text)['token']
identityId = json.loads(r1.text)['identityId']
cognito_client = boto3.client('cognito-identity', region_name=region)
response = cognito_client.get_credentials_for_identity(IdentityId=identityId, Logins={'cognito-identity---cn-north-1.amazonaws.com.rproxy.goskope.com.cn':token})
sessionToken=response['Credentials']['SessionToken']
accessKeyId=response['Credentials']['AccessKeyId']
secretKey=response['Credentials']['SecretKey']
iot_client = boto3.client('iot',region_name=region,aws_access_key_id=accessKeyId,aws_secret_access_key=secretKey,aws_session_token=sessionToken)
response = iot_client.attach_policy(
    policyName=policy_name,
    target=identityId
)

# Custom MQTT message callback
def customCallback(client, userdata, message):
    print("Received a new message: ")
    print(message.payload)
    print("from topic: ")
    print(message.topic)
    print("--------------\n\n")

def pub_msg():
    try:
        pri_loopCount = 0
        while True:
            print 'please input:',
            msg = raw_input()
            private_data = msg
            message = {}
            message['message'] = json.dumps({"source":device_name, "data":private_data})
            message['sequence'] = pri_loopCount
            messageJson = json.dumps(message)
            myAWSIoTMQTTClient.publish(private_topic, messageJson, 1)
            pri_loopCount += 1
            time.sleep(2)
    except:
        sys.exit()

if __name__ == '__main__':
    # Init AWSIoTMQTTClient
    myAWSIoTMQTTClient = None
    myAWSIoTMQTTClient = AWSIoTMQTTClient(device_name, useWebsocket=True)
    myAWSIoTMQTTClient.configureEndpoint(endpoint, port)
    myAWSIoTMQTTClient.configureCredentials(server_root_ca_file)
    myAWSIoTMQTTClient.configureIAMCredentials(accessKeyId, secretKey, sessionToken)

    # AWSIoTMQTTClient connection configuration
    myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
    myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)  # Infinite offline Publish queueing
    myAWSIoTMQTTClient.configureDrainingFrequency(2)  # Draining: 2 Hz
    myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)  # 10 sec
    myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)  # 5 sec

    # Connect and subscribe to AWS IoT
    myAWSIoTMQTTClient.connect()
    myAWSIoTMQTTClient.subscribe(private_topic, 1, customCallback)
    time.sleep(2)

    pub_msg()
EOF

运行设备模拟程序。

python device_cognito_websocket.py \
--developer_provicer_endpoint "http://127.0.0.1:8383/login/" \
--iot_policy_name IoTPolicyForDeviceCognitoWebsocket \
--endpoint_prefix ${endpoint_prefix}

此设备模拟程序会一直运行,接受输入的数据,发送到 AWS IoT Core,同时也订阅自己发送消息的 topic。

输入要发送到 AWS IoT 的消息,如 “data from device cognito websocket.”,设备会接收到自己发送的这个消息。同时,在本系列文章第一篇的 4.3 章节中打开的控制台中也可以看到此消息。

执行 Ctrl+C 停止程序。

3. 资源清理(可选)

aws iam detach-role-policy --role-name IoTDeviceRoleInCognito --policy-arn arn:aws-cn:iam::${account_id}:policy/IoTPolicyManager
aws iam detach-role-policy --role-name IoTDeviceRoleInCognito --policy-arn arn:aws-cn:iam::${account_id}:policy/IoTDeviceCognitoHttpPolicy
aws iam detach-role-policy --role-name IoTDeviceRoleInCognito --policy-arn arn:aws-cn:iam::${account_id}:policy/IoTDeviceCognitoWebsocketPolicy
aws iam delete-policy --policy-arn arn:aws-cn:iam::${account_id}:policy/IoTDeviceCognitoHttpPolicy
aws iam delete-policy --policy-arn arn:aws-cn:iam::${account_id}:policy/IoTPolicyManager
aws iam delete-policy --policy-arn arn:aws-cn:iam::${account_id}:policy/IoTDeviceCognitoWebsocketPolicy
aws iam delete-role --role-name IoTDeviceRoleInCognito
aws cognito-identity delete-identity-pool --identity-pool-id $IdentityPoolId
sed -i '/IdentityPoolId/d' ~/.bashrc
aws iam detach-user-policy --user-name developerIdpUser --policy-arn arn:aws-cn:iam::${account_id}:policy/developerIdpPolicy
aws iam delete-policy --policy-arn arn:aws-cn:iam::${account_id}:policy/developerIdpPolicy
AccessKeyId=`cat /tmp/IoT_demo_access_key2 | jq .AccessKey.AccessKeyId| sed 's/"//g'`
aws iam delete-access-key --access-key-id $AccessKeyId --user-name developerIdpUser
aws iam delete-user --user-name developerIdpUser
rm -f /tmp/IoT_demo_access_key2
target1=`aws iot list-targets-for-policy --policy-name IoTPolicyForDeviceCognitohttp | jq .targets[0] | sed 's/"//g'`
aws iot detach-policy --policy-name IoTPolicyForDeviceCognitohttp --target $target1
aws iot delete-policy --policy-name IoTPolicyForDeviceCognitohttp
target2=`aws iot list-targets-for-policy --policy-name IoTPolicyForDeviceCognitoWebsocket | jq .targets[0] | sed 's/"//g'`
aws iot detach-policy --policy-name IoTPolicyForDeviceCognitoWebsocket --target $target2
aws iot delete-policy --policy-name IoTPolicyForDeviceCognitoWebsocket

本篇作者

陈雷

AWS 高级解决方案架构师,负责基于 AWS 的云计算方案的咨询与架构设计,同时致力于物联网方面的研究和推广。在加入 AWS 之前,有超过 10 年的架构设计和软件开发的经验,在微服务,devops,物联网和大数据领域有丰富的经验。