Android APP接入AWS IoT实现发布订阅测试详解

文章目录

  • 一、前言
  • 二、准备
  • 三 、编码
  • 四、踩坑
  • 总结

  •  产品需要接入物联网,主要面向欧美市场,所以选择使用了亚马逊物联网平台。研究了一阵子做一些总结。
    

    一、前言

    本文中是开发的APP控制端(开发语言是Android),来接入AWS IOT平台并且实现消息的订阅和发布。由于能力有限,若有错误的理解请指正。
    AWS IOT的官方文档,不得不说文档写的真“详细”啊,看得云里雾里,只能参考互联网。
    由于国内使用AWS物联网平台应用不多,关于Android APP的demo更是几乎没有,网上能搜到与之相关最多的是PubSub的Android项目。AWS的官方博客中有一篇文章介绍了相关内容,还有其他一些博主也有一些探究,但由于时间过于久远很多操作都发生了变化,甚至连官方GitHub上的Android PubSub示例也下架了(猜测早期应该是有的),资料实在是匮乏,只能一步一步尝试。

    二、准备

    在编写APP之前首先要了解一些基础知识,如mqtt协议、IOT设备、事务、策略等等,本文中还使用了AWS IAM、Cognito,也需要做适当了解。

    1.使用Android Studio构建App项目
    项目下的build.gradle中导入iot使用包,AWS的开发包很多,这里使用的android版的iot开发包,服务器端应该使用java版

    implementation 'com.amazonaws:aws-android-sdk-iot:2.73.0'
    
    

    2.注册AWS账号,需要一张信用卡,国内的也可以,没有什么难度。审核通过后即可开始测试使用。
    登录AWS控制台,主要使用的是IOT Core、IAM以及Cognito
    在使用IOT Core以及Cognito控制台时需要注意右上角的区域选择,曾经我就犯过在代码里创建资源后在控制台里始终找不到的错误,原因就是区域错了。另外各区域是否都支持用户池、身份池暂时不明,我就直接选择的us-east-1测试。

    3.Cognito、IAM概念
    从官方文档阅读可以知道AWS IOT Core允许好几种方式来安全接入,比如X.509证书、Cognito身份等等。
    从网上搜索的关于之前的PubSub项目的零散代码可以看出:

     //创建证书
                        CreateKeysAndCertificateRequest createKeysAndCertificateRequest =
                                new CreateKeysAndCertificateRequest();
                        createKeysAndCertificateRequest.setSetAsActive(true);
    
                        final CreateKeysAndCertificateResult createKeysAndCertificateResult;
                        //调用了refresh方法
                        createKeysAndCertificateResult =
                                mIotAndroidClient.createKeysAndCertificate(createKeysAndCertificateRequest);
                        Log.i(Constants.TAG,
                                "Cert ID: " +
                                        createKeysAndCertificateResult.getCertificateId() +
                                        " created.");
    
    //                        store in keystore for use in MQTT client
    //                        saved as alias "default" so a new certificate isn't
    //                        generated each run of this application
                        Log.d(Constants.TAG,certificateId);
                        Log.d(Constants.TAG,createKeysAndCertificateResult.getCertificatePem());
                        Log.d(Constants.TAG,createKeysAndCertificateResult.getKeyPair().getPrivateKey());
    
                        AWSIotKeystoreHelper.saveCertificateAndPrivateKey(certificateId,createKeysAndCertificateResult.getCertificatePem(),
                                createKeysAndCertificateResult.getKeyPair().getPrivateKey(),
                                keystorePath, keystoreName, keystorePassword);
    

    该示例中实际是创建了一个Keystore文件存储在App的私有目录下,并且给该Keystore附加上一个IOT策略(一定要加,不加连接不了),然后调用manager去连接,如下:

    mqttManager.connect(pubSubActivity.clientKeyStore ...
    

    在连接成功后即可订阅、发布消息了。
    整个过程可以概括为:
    1).app内有权限的情况下生成一个iot认可的证书,且该证书上附加了一个已存在的策略
    2).iot认可这个附上策略的证书,可连接发布订阅消息

    但是这种方案不适用于实际生产环节,且官方也不推荐使用(keystore文件相当于在手机上存放了一个证书),这种方案应该更适用于设备端接入AWS IOT。
    在每次生成Keystore文件时在IOT的后台下的证书菜单里,都会对应有一个证书。而手机APP一旦卸载之前生成的Keystore文件也会消失,再次调用会重新生成新的证书,旧的证书依然存在但其实已失效(刚开始测试使用这种方案时App经常卸载重装,iot的后台的证书菜单下就多了很多无用的数据)。

    对于移动端APP推荐使用Cognito身份来接入AWS IOT平台。
    Cognito没有做细致研究,仅使用了身份池。身份池的概念相当于初始进入APP时是一个游客身份,经过“登录”后变成了另一个经过验证的身份,所拥有的权限也发生了变化。我测试了身份提供商是Login with Amazon和自定义身份,其他如用户池、google等请自行测试。

    4.步骤详解
    1)配置身份池

    这里经过身份验证的我选择了Amazon和自定义,下一步生成对应的IAM角色,直接生成新的角色,分别命名auth_xx和unauth_xx,具体名称不限只需要记住哪个对应哪个就行。

    输入
    Amazon身份提供商在该处填写应用程序ID,该应用ID是在Login With Amazon控制台注册生成的ID,还是有点麻烦的,请自行查看官网文档进行注册。

    最终填写的是图示中箭头的这个id。

    自定义身份提供商可自己定义一个标识,合法即可,这个标识后续在android和自有服务器端都会使用到。

    填写好后其他默认不需要修改,直接创建审核通过即可。

    2)补充IAM角色策略

    进入IAM控制台后点击角色,找到上面创建身份池时命名的两个角色名。
    点击给经过验证的auth_xx的这个角色,点击附加策略。

    测试可以点击附加策略后选择AWSIoTFullAccess全部权限,也可以点击创建内联策略自定义一个,json格式串可以网上搜,自定义主要可以缩小一些权限范围,后面再讲权限策略设置。

    至此可以简单理解下:一个用户下载了APP,刚使用时是一个游客,当用户使用了Amazon账户联合登录或者服务器端对他进行了验证合法后用户就获得了auth_xx这个角色的权限,就能和IOT互动了。

    3)创建IOT Policy
    为什么还需要创建Iot Policy呢,因为官方文档里介绍这种接入AWS IOT的方式是由IAM角色的权限策略和这个identity身份上的权限策略组合而定的,至于两者的权限是如何合并界定参考官方,我也没细致研究。(题外话:如果给unauth_xx身份直接赋予了AWSIoTFullAccess权限是后不用附加IOT策略也是可以连接IOT平台的)
    点击安全性->策略,创建一个新策略,内容也可以先设置全部权限,稍后和上面IAM一样再进行细分。

    三 、编码

    下面介绍以Login with Amzon为身份提供商接入AWS IOT 
     已准备好的工作:
       1>创建了一个身份池,关联定义了两个IAM角色,一个是未经过认证的角色,权限默认不需要更改,另一个是经过认证的角色,附加上了AWSIoTFullAccess权限。该身份提供商Amazon的ID已填写。
       2>Iot的后台创建了一个策略,策略先定义的全部权限(参照上图)
       3>注意区域选择一致
    

    Android App中接入Login with Amazon不做详解,网上资料齐全没什么难度。
    在oncreat方法里先初始化Cognito认证构造方法

    credentialsProvider = new CognitoCachingCredentialsProvider(
                    getApplicationContext(), // Context
                    "XXXXXXXXXXXX", //Cognito控值台创建的身份池ID
                    Regions.US_EAST_1 // 身份池所属区域
            );
    

    题外话:如果是使用的自定义身份验证的方式,就不能使用这个构造方法了,要使用下面这个

    DeveloperAuthenticationProvider developerProvider = new DeveloperAuthenticationProvider
                    (null, Cognito_Pool_ID, Regions.US_EAST_1,baseApplication);
    credentialsProvider = new CognitoCachingCredentialsProvider( context, developerProvider, Regions.US_EAST_1 );
    

    developerProvider是需要自己实现的一个方法,继承自AWSAbstractCognitoDeveloperIdentityProvider

    public class DeveloperAuthenticationProvider extends AWSAbstractCognitoDeveloperIdentityProvider {
    
        private static final String developerProvider = "xxxxxx";//这是自定义的身份提供商名
        private BaseApplication baseApplication;
    	//构造方法
        public DeveloperAuthenticationProvider(String accountId, String identityPoolId, Regions region, BaseApplication baseApplication) {
            super(accountId, identityPoolId, region);
            this.baseApplication = baseApplication;
        }
        .............
    

    这个自定义类中主要是要实现refresh方法

    @Override
        public String refresh() {
            String result = "";
            String getIdenttid = "";
            String token="";
            try {
                //这里要调用后台服务器方法来获取权限,这里就简单传了存在sp中的用户ID到后台服务器
                String appToken = (String)baseApplication.get("token","");
                result = AcceletNetWork.synGet(HttpUrls.GetCognitoToken+"&appToken="+appToken);//这里采用的是同步
                Gson gson = new Gson();
                TokenDataBean dataBean = gson.fromJson(result, TokenDataBean.class);
                getIdenttid = dataBean.getIdentityId();
                token = dataBean.getToken();
            } catch (Exception e) {
                e.printStackTrace();
            }
            //服务器返回两个参数,update一下
            update(getIdenttid,token);
            return token;
        }
    

    服务器也是java编写,代码简单提一下:
    服务器使用的AWS java版的SDK,且有V1、V2的版本,请注意区别。

    try {
    			Map<String, String> logins = Collections.singletonMap(developerIdentityProviderName, "single" + ":" + user_sn);//developerIdentityProviderName是在身份池里填写的自定义标识,第二个参数是唯一标识,这里采用的是用户user_sn
    			Builder builder = GetOpenIdTokenForDeveloperIdentityRequest.builder()
    					.identityPoolId(identityPoolId)//身份池ID
    					.logins(logins)
    					.tokenDuration(3600*24L);//有效期
    			GetOpenIdTokenForDeveloperIdentityRequest request = builder.build();
    			
    			GetOpenIdTokenForDeveloperIdentityResponse response = getClient().getOpenIdTokenForDeveloperIdentity(request);
    	//最主要的就是getOpenIdTokenForDeveloperIdentity方法了,官网文档介绍一定要从后台调用
    			
    			if(response != null) {
    				JSONObject identityJson = new JSONObject();
    				identityJson.put("identityId", response.identityId());
    				identityJson.put("token", response.token());
    				return identityJson;
    			}			
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    

    题外话结束,回到LWA版。在使用亚马逊账户登录完成后会返回一个token,更新登录映射

    public void onSuccess(AuthorizeResult result) {
                            String token = result.getAccessToken();
                            if (null != token) {
                                /* The user is signed in */
                                Map<String, String> logins = new HashMap<String, String>();
                                logins.put("www.amazon.com", token);//标识名固定为www.amazon.com
                                credentialsProvider.setLogins(logins);//调用setLogins
                           		attachPolicy();
                            } else {
                                /* The user is not signed in */
                            }
                        }
    

    然后附加IOT策略

     private void attachPolicy() {
            try{
                AWSIotClient awsIotClient = new AWSIotClient(credentialsProvider);
                awsIotClient.setRegion(Region.getRegion(Regions.US_EAST_1));
    
                AttachPolicyRequest attachPolicyRequest = new AttachPolicyRequest()
                        .withPolicyName("xxxxx")//IOT控制台里创建的策略名称
                        .withTarget(credentialsProvider.getIdentityId());//身份池的唯一ID
                awsIotClient.attachPolicy(attachPolicyRequest);
            }catch (Exception e){
                e.printStackTrace();
                Log.d(Constants.TAG,"附加策略异常");
            }
    
        }
    

    附加IOT策略说明:网上搜有不少介绍使用AWS CLI给identityId加上策略,更有甚者说没有API可调用的。
    还是得参考官方文档啊,文档里介绍了核心代码但就是不提供完整的PubSub示例,无语。
    这里附加IOT策略需要注意:
    1>构造的awsIotClient需要有attachPolicy的权限,之前IAM里经过身份验证的角色赋予了AWSIOTFull全部的权限,所以没问题,但是你如果改了那就要加上AttachPolicy。
    2>IOT控制台里创建的策略名称也要正确,如果区域不一致会报找不到策略的错误。如果是控制台里复制时注意左右不要有空格。
    3>identityId身份,IOT策略附加到这个角色身上,这个身份对应的一个唯一ID,在Cognito管理后台可以查看。

    这个身份ID很有用处,后面要用它连接AWS IOT CORE以及解决缓存问题。
    4>由于attachPolicy方法没有回调函数之类的反应是否策略附加成功,各个管理后台也没有查看的地方。但我试了修改策略名称或者随便填一个身份ID,都会报错,所以只要程序没有报错那么策略就附加成功了。

    接下来就是准备连接AWS IOT进行MQTT测试了,初始化awsIotMqttManager并进行连接

    				String tIdentityId= credentialsProvider.getIdentityId();
                    awsIotMqttManager = new AWSIotMqttManager(tIdentityId, "xxxxxxxxxx");
                    awsIotMqttManager.setCleanSession(true);//默认为true,类似于一个连接如果断线后服务器不再保留其相关的信息
                    awsIotMqttManager.setKeepAlive(10);// 将keepalive的时间设置小于服务端的超时时间,则客户端每隔 keepalive的时间就会给服务端发一个心跳包,默认是300s,单位是s
                    awsIotMqttManager.setAutoReconnect(true);//断开后自动重连
                    awsIotMqttManager.setMaxAutoReconnectAttempts(1);//设置断网后重新尝试连接的次数,不设的话默认值为-1 ,一直试着连接
    
                    try {
                        awsIotMqttManager.connect(credentialsProvider, (status, throwable) -> {
                            if(throwable!=null){
                                Log.d(Constants.TAG,throwable.toString());
                            }
                            if (status == AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Connecting) {
                                Log.d(Constants.TAG,"正在连接中");
                                
                            }else if(status == AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Connected){
                                Log.d(Constants.TAG,"连上了");
                            }else if (status == AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Reconnecting) {
                                Log.d(Constants.TAG,"重连");
                            }else if (status == AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.ConnectionLost) {
                                Log.d(Constants.TAG,"失去连接");
                            }else{
                                Log.d(Constants.TAG,"未知状态");
                            }
                        });
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                        Log.d(Constants.TAG, "还没有权限操作" );
                    }
    

    AWSIotMqttManager构造方法传入两个参数,第一个client_id,这个可以是任意的string类型。我这里使用的唯一身份ID,之所以这样使用是因为我配置了权限细分策略里规定我只能使用这个唯一ID才能连接,其他都不可以。之前的示例里使用的UUID.randomUUID().toString()产生随机值作为唯一client_id也是可以的,但也存在两个使用APP的client_id一致的风险,只不过几乎不可能罢了。实际生产环境中这个可不能写成固定值,同一个clientid的话后连的会把先连的给踢掉。第二个参数在IOT控制台里的设置菜单里终端节点。
    如果不出意外就可以连接上了,需要注意的是若连不上报下面错误(不得不吐槽AWS各种诡异错误异常都难以定位到底是哪里出错了)

    MqttException (0) - java.io.IOException: Already connected
    

    不要参照这篇博客里介绍的直接给IAM中的Auth_xx身份加上个AdministratorAccess权限。根本上虽然都是权限的问题但是需要了解怎么改,这里直接加上AdministratorAccess是不可取的。

    awsIotMqttManager有一些可以的属性可以修改,比较有印象的就是setMaxAutoReconnectAttempts方法,刚开始连接失败后一直就不断的重连,后来找到这个方法,设置重连次数后程序就会按照规定的值进行尝试,一直失败直接退出。

    然后便是awsIotMqttManager.connect(credentialsProvider…这个方法,这里选择了定义的cognitor联合认证身份的方法,而非采用本地创建存储keystore文件的方法,随取随用。

    最后便是订阅和发布了,没有什么难度,要注意的是需要先连接上AWS IOT。

    订阅:

    				try{
                        awsIotMqttManager.subscribeToTopic("esp32/pub", AWSIotMqttQos.QOS0, new AWSIotMqttSubscriptionStatusCallback() {
                            @Override
                            public void onSuccess() {
                                Log.d(Constants.TAG,"订阅esp32/pub成功!!");
                            }
    
                            @Override
                            public void onFailure(Throwable exception) {
                                Log.d(Constants.TAG,"订阅esp32/pub失败!!");
                                Log.d(Constants.TAG,exception.toString());
                            }
                        }, (topic, data) -> {
                            String str = new String(data);
                            Log.d(Constants.TAG,"esp32/pub:"+str);
                        });
                    }catch (Exception e){
                        e.printStackTrace();
                    }
    

    发布:

    try{
                        awsIotMqttManager.publishData("avs".getBytes(), "esp32/pub", AWSIotMqttQos.QOS0, new AWSIotMqttMessageDeliveryCallback() {
                            @Override
                            public void statusChanged(MessageDeliveryStatus status, Object userData) {
                                if(status == MessageDeliveryStatus.Fail){
                                    Log.d(Constants.TAG,"发布esp32/pub失败!!");
                                }else if(status == MessageDeliveryStatus.Success){
                                    Log.d(Constants.TAG,"发布esp32/pub成功!!");
                                    Log.d(Constants.TAG,userData.toString());
                                }
                            }
                        },"success");
                    }catch (Exception e){
                        e.printStackTrace();
                    }
    

    至此一个完整的手机端APP连接AWS IOT并且发布订阅示例结束。

    四、踩坑

    过程中遇到了不少问题,由于网上资料实在是匮乏,只能靠自己想办法,Android原生开发调试又特麻烦。

    1>身份池和iot资源位置要统一,犯过创建资源成功但位置搞错了。
    2>这种使用Cognito联合身份连接AWS IOT一定是IAM的角色策略和IOT策略结合在一起才能使用
    3>权限细分,这一部分困扰了很长一段时间
    像上述例子中如果IAM的Auth_xx角色赋予了AWSIoTFullAccess权限,IOT策略中又赋予了iot*的全部权限,虽然可以正常连接AWS IOT 且可以向任何topic发布消息,也可以订阅和接收任何topic的消息,这明显与实际使用场景不符。
    关于实际使用场景中的权限细分可以阅读这篇文章,从文章中介绍可以看出通常做法是IAM的Auth_xx角色给个宽松的权限,iot策略中再做进一步缩小限制。

    我所测试细分的权限
    1)谁能连,app端不应该使用UUID.randomUUID().toString()这种随机值进行标识,而是使用了identityId,即使用户换个手机登录也能识别到是谁。可能涉及到保留消息等相关知识,没有细致研究。
    这个ID在策略里会转化成一个变量,${cognito-identity.amazonaws.com:sub},AWS会把他自动转化成实际的ID
    2)能订阅和发布哪些topic
    注意订阅的权限通常要和接收权限一起设置,不然订阅成功了但却收不到消息
    3)要正确配置各个权限的策略资源(就是arn开头的那一串)

    我提供我做权限测试细分所配置的两个策略
    IAM中的Auth_xx角色策略:

    这里我给的相对比较宽松的策略,注意AttachPolicy一定要加,不然Android程序里attachPolicy会报没有权限错误。
    AWS IOT策略:

    这里就做进一步权限限制了,必须使用credentialsProvider.getIdentityId()才能连,使用UUID.randomUUID().toString()被拒绝。
    只能订阅esp32/pub和esp32/pub/demo两个topic(实际生产环境中应参考iot中的示例使用通配符啥的),但只能接收esp32/pub/demo这个topic的消息。
    发布消息的同样只允许向esp32/pub和esp32/pub/demo这两个topic发送
    实际demo效果是向这两个topic都能发送成功消息,但只有esp32/pub/demo这个topic接收到了发布的消息。
    需要注意的是:
    先要订阅成功后才能接收发布消息
    箭头所指的各个标识要写正确,该是哪个就是哪个,控制台拷贝别处的json过来也不会出错* 我这里的资源位置以及账号全部用*代替,可替换实际的值

    4>其他
    我当初在做限制只能以IdentityId进行连接AWS IOT时,有可能由于IAM策略 IOT策略频繁更新,明明配置正确了但还是连接不上,试了好久都不行。后台无意间将Cognito控制台下的那个身份ID给删除了,然后就可以正常连接了。这个方案暂时不确定其是否有效,猜测可能与AWS 的缓存机制有关。

    订阅和发布报错,明明我的配置是按照官方的示例写的,但却没法订阅也没法发布,也没法精准定位究竟什么原因。折腾后来发现其实是自己的topic写错了,比如订阅的topic应该是esp32/pub/demo,但我写成了/esp32/pub/demo

    总结

    这里仅介绍了初步的Android APP使用AWS IOT的例子,代码就不上传了,各位自行测试。
    另外这只是初步的研究,实际生产环节中还涉及到比如分享等权限设置。

    物联沃分享整理
    物联沃-IOTWORD物联网 » Android APP接入AWS IoT实现发布订阅测试详解

    发表评论