亚马逊AWS官方博客

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

1. 简介

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

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

2. 设备使用自定义身份验证接入

您还可以使用自定义的身份验证方法,通过自定义的 Authorizer 来认证设备。自定义的 Authorizer 会触发一个 Lambda  函数来对设备进行认证。 Lambda  函数返回一个 IoT Policy,AWS IoT 根据这个 IoT Policy 来对请求进行鉴权。自定义身份验证方式非常灵活,可以实现自己的特殊需求,例如用在一些有历史遗留问题的系统中,或者设备受到某些特殊的限制而不能使用其他认证方式等场景中。

设备使用自定义身份验证的流程示意图如下:

2.1 部署自定义身份验证的 Authorizer

自定义身份验证是由 Lambda 函数来认证授权,所以先要创建 Lambda 函数。

创建 Lambda 要代入的角色。

IoTDemoAuthorizerFunctionRoleArn=`aws iam create-role \
--role-name IoTDemoAuthorizerFunctionRole \
--assume-role-policy-document "{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {
      \"Effect\": \"Allow\",
      \"Principal\": {
        \"Service\": \"lambda.amazonaws.com\"
      },
      \"Action\": \"sts:AssumeRole\"
    }
  ]
}" | jq .Role.Arn | sed 's/"//g'`

为 Lambda 角色绑定一个 IAM Policy 。

aws iam attach-role-policy --role-name IoTDemoAuthorizerFunctionRole \
--policy-arn arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

创建 Lambda 函数。

cat <<EOF > ~/awsIoTAccessDemo/IoTDemoAuthorizerFunction.py
from __future__ import print_function

import json
import base64

def lambda_handler(event, context):
    try:
        token = json.loads(base64.b64decode(event['token']))
        device_id = token['device_id']
        policyDocuments = []
        policyDocument = {}
        policyDocument['Version'] = '2012-10-17'
        policyDocument['Statement'] = []
        statement0 = {}
        statement0['Action'] = []
        statement0['Action'].append('iot:Publish')
        statement0['Action'].append('iot:Receive')
        statement0['Effect'] = 'Allow'
        statement0['Resource'] = "arn:aws-cn:iot:cn-north-1:*:topic/IoTDemo/"+device_id
        policyDocument['Statement'].append(statement0)
        statement1 = {}
        statement1['Action'] = 'iot:Subscribe'
        statement1['Effect'] = 'Allow'
        statement1['Resource'] = "arn:aws-cn:iot:cn-north-1:*:topicfilter/IoTDemo/"+device_id
        policyDocument['Statement'].append(statement1)
        statement2 = {}
        statement2['Action'] = 'iot:Connect'
        statement2['Effect'] = 'Allow'
        statement2['Resource'] = "arn:aws-cn:iot:cn-north-1:*:client/"+device_id
        policyDocument['Statement'].append(statement2)
        policyDocuments.append(policyDocument)
        authResponse = {}
        authResponse['isAuthenticated'] = True
        authResponse['principalId'] = device_id.replace('_','')
        authResponse['disconnectAfterInSeconds'] = 3600
        authResponse['refreshAfterInSeconds'] = 600
        authResponse['policyDocuments'] = policyDocuments
        print(str(authResponse))
        return authResponse
    except Exception as e:
        print(str(e))
        return {}        
EOF
zip function.zip  IoTDemoAuthorizerFunction.py
IoTDemoAuthorizerFunctionArn=`aws lambda create-function \
--function-name IoTDemoAuthorizerFunction \
--zip-file fileb://function.zip --handler IoTDemoAuthorizerFunction.lambda_handler \
--runtime python2.7 --role ${IoTDemoAuthorizerFunctionRoleArn} \
| jq .FunctionArn | sed 's/"//g'` 

创建用于验证 Token 的密钥对。

openssl genrsa -out authorizer_private.pem 2048
openssl rsa -in authorizer_private.pem -outform PEM -pubout -out authorizer_public.pem

创建 Authorizer。

authorizerArn=`aws iot create-authorizer \
--authorizer-name IoTDemoAuthorizer \
--authorizer-function-arn ${IoTDemoAuthorizerFunctionArn} \
--token-key-name IoTDemoAuthorizerToken \
--token-signing-public-keys FIRST_KEY="\`cat authorizer_public.pem\`" \
--status ACTIVE | jq .authorizerArn | sed 's/"//g'`

为 Authorizer 配置调用 Lambda 的权限。

aws lambda add-permission --function-name IoTDemoAuthorizerFunction \
--statement-id IoTDemoAuthorizerFunctionPermission \
--action 'lambda:InvokeFunction' \
--principal iot.amazonaws.com \
--source-arn ${authorizerArn}

2.2 设备使用 HTTP 协议接入

生成设备模拟程序。

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

import argparse
import json
import requests
import commands
import base64

#获取参数
parser = argparse.ArgumentParser(description='Send data to IoT Core')
parser.add_argument('--data', default="data from device_custom_auth_http.",
            help='data to IoT')
parser.add_argument('--authorizer_name', required=True,
            help='custom authorizer name.')
parser.add_argument('--endpoint_prefix', required=True,
            help='your iot endpoint prefix.')
parser.add_argument('--private_key', required=True,
            help='your custom authorizer private key.')

args = parser.parse_args()
private_data = args.data
endpoint_prefix = args.endpoint_prefix
authorizer_name = args.authorizer_name
private_key = args.private_key
device_name = 'device_custom_auth_http'
region = 'cn-north-1'
private_topic = "IoTDemo/"+device_name
iot_endpoint = "https://%s---iot---cn-north-1.amazonaws.com.rproxy.goskope.com.cn/topics/" % endpoint_prefix
token = {"device_id":device_name}
token_str = base64.b64encode(json.dumps(token))
command = "/bin/echo -n %s | openssl dgst -sha256 -sign %s 2>/dev/null| openssl base64 2>/dev/null" % (token_str,private_key)
return_code, return_str = commands.getstatusoutput(command)
signature = return_str.strip().replace('\n','')
headers = {
    "Content-Type": "application/json",
    "IoTDemoAuthorizerToken":token_str,
    "X-Amz-CustomAuthorizer-Signature":signature,
    "X-Amz-CustomAuthorizer-Name":authorizer_name
}
data = json.dumps({"source":device_name, "data":private_data})
endpoint = iot_endpoint + private_topic
r1 = requests.post(endpoint, data=data, headers = headers)
EOF

运行设备模拟程序。

需要注意的是目前实际测试自定义身份验方式下,不支持 ATS endpoint,代码中需注意。另外自定义身份验证也暂时没有 Python SDK 的支持,需要自己编写相关的代码。

python device_custom_auth_http.py \
--data "data from device custom authentication http." \
--authorizer_name IoTDemoAuthorizer \
--endpoint_prefix ${endpoint_prefix} \
--private_key authorizer_private.pem

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

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

生成设备模拟程序。

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

import json
import commands
import sys
import argparse
import base64
import time
from paho.mqtt.client import Client
import threading

#获取参数
parser = argparse.ArgumentParser(description='Send data to IoT Core')
parser.add_argument('--authorizer_name', required=True,
            help='custom authorizer name.')
parser.add_argument('--endpoint_prefix', required=True,
            help='your iot endpoint prefix.')
parser.add_argument('--private_key', required=True,
            help='your custom authorizer private key.')

args = parser.parse_args()
authorizer_name = args.authorizer_name
endpoint_prefix = args.endpoint_prefix
private_key = args.private_key
device_name = 'device_custom_auth_websocket'
region = 'cn-north-1'
private_topic = "IoTDemo/"+device_name
iot_endpoint = "%s---iot---cn-north-1.amazonaws.com.rproxy.goskope.com.cn" % endpoint_prefix  #ATS不支持!!!!!
ca_certs_file = "./VeriSign-Class 3-Public-Primary-Certification-Authority-G5.pem"

# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    #print("Connected with result code "+str(rc))
    client.subscribe(private_topic)

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    print("receive message from topic "+msg.topic+", message is "+str(msg.payload))

# This is specific to custom authorizer setup
token = {"device_id":device_name}
token_str = base64.b64encode(json.dumps(token))
command = "/bin/echo -n %s | openssl dgst -sha256 -sign %s 2>/dev/null| openssl base64 2>/dev/null" % (token_str,private_key)
return_code, return_str = commands.getstatusoutput(command)
signature = return_str.strip().replace('\n','')
aws_headers = {
    "IoTDemoAuthorizerToken":token_str,
    "X-Amz-CustomAuthorizer-Signature":signature,
    "X-Amz-CustomAuthorizer-Name":authorizer_name
}
client = Client(device_name, transport="websockets")
client.ws_set_options(headers=aws_headers)
client.tls_set(ca_certs = ca_certs_file)
client.on_connect = on_connect
client.on_message = on_message
client.connect(iot_endpoint, 443, 60)
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)
            client.publish(private_topic, messageJson, 1)
            pri_loopCount += 1
            time.sleep(2)
    except:
        sys.exit()

t = threading.Thread(target=client.loop_forever,args=())
t.setDaemon(True)
t.start()
pub_msg()

EOF

运行设备模拟程序。

python device_custom_auth_websocket.py \
--endpoint_prefix ${endpoint_prefix} \
--authorizer_name IoTDemoAuthorizer \
--private_key authorizer_private.pem

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

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

执行 Ctrl+C 停止程序。

3. 资源清理(可选)

aws iam detach-role-policy --role-name IoTDemoAuthorizerFunctionRole --policy-arn arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name IoTDemoAuthorizerFunctionRole
aws lambda delete-function --function-name IoTDemoAuthorizerFunction
aws iot update-authorizer --authorizer-name IoTDemoAuthorizer --status INACTIVE
aws iot delete-authorizer --authorizer-name IoTDemoAuthorizer
rm -f ~/awsIoTAccessDemo/*

4. 总结

本系列文章介绍了 AWS IoT 支持的所有协议,认证和授权方式,及其各种接入场景下具体的实现细节。这可以帮助我们选择更合适的方式连接到 AWS IoT,也可以让我们更快的把设备接入到 AWS IoT 中。

本篇作者

陈雷

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