亚马逊AWS官方博客

Amazon Cognito集成Login with Amazon详解

作者:薛峰,亚马逊AWS解决方案架构师

背景介绍

Amazon Cognito可以为我们移动开发中的终端用户维护唯一标识符,跨不设备和平台维护用户登录的一致。Cognito还可以为我们的应用提供限制权限的临时凭据来访问 AWS的资源。

使用Amazon Cognito我们的应用可以支持未验证用户,以及使用公开的身份提供方来验证用户,目前支持的身份提供方包括Facebook, Google 和Login with Amazon。

未验证的用户绑定到设备,即通过Cognito客户端SDK在用户使用相同设备时为他们维护唯一标识符。而已验证的用户则可以跨设备维护唯一标识符,即使他们使用iOS和Android这样不同的操作系统。

今天我们通过一个Android开发实例,详细讲解Amazon Cognito 与 Login with Amazon 集成,以针对移动应用程序和 Web 应用程序用户提供联合身份验证。

Login with Amazon
使用Amazon账号登录,可以省去注册账号的繁琐,使用用户已经熟练使用的账号直接登录。借助Amazon.com相同的验证机制,可以轻松享受其健壮的安全性和可扩展性。开发者可以不必自己再构建用户管理系统,而集中精力于自己的产品。Login with Amazon使用业界主流的OAuth 2.0标准,方便更快速地接入开发,也基于此Amazon Cognito也可以方便地接入。

我们使用当前主流的 Android Studio,为了方便调试先使用模拟器进行开发和演示。这些基础工作请大家自行准备好。

注册Login with Amazon
首先需要注册成Amazon开发者,然后到以下网址注册一个应用。(http://login.amazon.com/manageApps)

左上角Applications 模块下点击“Register New Application”按钮。Name 和 Description按自己需求填写。

Privacy Notice URL这是在登录时显示给用户的隐私协议页面,生产环境中需要是你的网站的一个页面。这里我们可以使用演示页面的URL https://www.example.com/privacy.html

Logo Image 这里是显示给用户的我们的应用图标,该图片会被自动缩小到50 x150像素,所以选择一个适当尺寸的图片上传。点击 save保存即可。

创建成功后,我们到该应用详情页,标题名称右下角有一行形如下面的应用ID:

App ID: amzn1.application.188a56d827a7d6555a8b67a5d

这个我们记下来,后面关联Amazon Cognito时会使用。

点击 Android Settings 展开,添加Android 设置。

Is this application distributed through the Amazon Appstore? 我们的应用只用于Cognitor验证,不需要上架Amazon Appstore,所以选 No.

Label 填写和前述Name相同值即可。

Package Name 填写后面我们创建Android App 时工程的包名,比如这里我们使用“com.example.cognito”。

Signature 填写App打包时的SHA-256签名值。详情可参考 (https://developer.android.com/tools/publishing/app-signing.html)

保存之后,这里会多出一项 API Key Value,我们点击“Get API Key Value”按钮,在弹出层显示一个长字符串,这个后续我们开发也会用到。

使用Login with Amazon开发

我们使用Android Studio 创建一个项目,先把Login with Amazon集成进来。

新建Android项目

启动Android Studio,创建一个新项目。

Application name 输入 Cognito。Company name 输入 example.com。这样下面显示的 Package name 刚好和我们前面创建 Login with Amazon 应用时填写的包名一致——“com.example.cognito”。

在目标设备选项页,Minimum SDK 选择 API 11 或以上,我这里选择的是 API 23。

Activity页选Empty Activity。最后Customize the Activity页保持默认,点击Finish按钮完成创建项目。

添加Login with Amazon SDK

从以下地址下载Login with Amazon SDK:(https://images-na.ssl-images-amazon.com/images/G/01/lwa/sdk/LoginWithAmazonSDKForAndroid._TTH_.zip)

解压到本地磁盘,找到 login-with-amazon-sdk.jar 文件备用。

我们在Project面板切换成Project 模式。把刚才解压出的login-with-amazon-sdk.jar 文件复制到 /app/libs 目录下,在这个文件上右键菜单点选Add As Library…,弹出 Create Library 对话框,直接点击 OK 按钮即可。

设置AndroidManifest.xml配置

我们这个项目需要访问互联网,在/app/src/main/AndroidManifest.xml 中根节点manifest下添加

<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

即可。

默认情况下屏幕转向或改变键盘状态会导致活动重启以重新配置界面,这样的重启会把我们的登录界面消失。我们把这个活动配置成手动变更就可以避免这个问题了。

在处理Login with Amazon的活动中,这里我们是默认的MainActivity,添加一个属性

android:configChanges="keyboard|keyboardHidden|orientation|screenSize"

用户点击按钮要登录时,API 会调起一个浏览器来展示登录页面,我们需要在项目中添加一个工作流WorkflowActivity。

和MainActivity所在的活动并列,添加以下代码

<activity android:name="com.amazon.identity.auth.device.workflow.WorkflowActivity"
            android:theme="@android:style/Theme.NoDisplay"
            android:allowTaskReparenting="true"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <!-- android:host must use the full package name found in Manifest General Attributes -->
                <data android:host="${applicationId}" android:scheme="amzn"/>
            </intent-filter>
        </activity>

添加Login with Amazon的API Key
我们在/app/src/main 下创建一个目录,名为assets,然后在这个目录里新建一个文本文件,名为api_key.txt。这是Login with Amazon的API默认读取API Key的方式。

回到我们的Login with Amazon 应用管理(https://sellercentral.amazon.com/gp/homepage.html)

点开我们刚才创建的应用,在 Android Settings 下面点击Get API Key Value 按钮

在弹出层中点击Select All 按钮把  API Key 选中,然后复制一下。

打开api_key.txt文件,把刚才复制的API Key粘贴进去。

添加Login with Amazon Button按钮
我们编辑activity_main.xml,添加一个标准ImageButton。给这个图片按钮设置一个id,比如:

android:id="@+id/login_with_amazon"

为这个按钮添加图片。

从( https://login.amazon.com/button-guide )可以下载默认的英文按钮或者简体中文的按钮。

解压出相应的图片文件,放到 /app/src/main/res/ 下相应图片目录中。给这个图片按钮设置

android:src="@drawable/btnlwa_gold_loginwithamazon.png"

构建和运行一下App,确认这个按钮已经正常显示出来了。

调用Login with Amazon SDK中的 API
我们首先需要一个RequestContext对象,建议做法是声明成一个private的属性,然后在活动的onCreate 方法中实例化。

private RequestContext requestContext;


@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    requestContext = RequestContext.create(this);

}

然后创建一个AuthorizeListener,它会监听并处理authorize 的调用,包含3个回调方法,如下创建出来然后注册到requestContext对象上。

requestContext.registerListener(new AuthorizeListener() {

    /* Authorization was completed successfully. */

    @Override

    public void onSuccess(AuthorizeResult result) {

        /* Your app is now authorized for the requested scopes */

    }


    /* There was an error during the attempt to authorize the

       application. */

    @Override

    public void onError(AuthError ae) {

        /* Inform the user of the error */

    }


    /* Authorization was cancelled before it could be completed. */

    @Override

    public void onCancel(AuthCancellation cancellation) {

        /* Reset the UI to a ready-to-login state */

    }

});

}

验证的流程是跳转到浏览器显示网页,登录成功后会回调onSuccess方法,但是也可能用户没有登录而是取消或者浏览到别处去了,所以这个接口有3个方法onSuccess, onError 和 onCancel。

为了适应Android 的应用生命周期管理,我们还需要在当前活动的onResume方法里加一段,以便用户没完成登录流程前操作系统把App关闭时,用户重新打开App时可以把验证界面恢复起来。

@Override

protected void onResume() {

    super.onResume();

    requestContext.onResume();

}

我们在刚才添加的按钮上注册 onClick 事件处理器调用AuthorizationManager的authorize方法来提示用户登录并授权我们的应用。

// 给登录按钮注册点击事件
mLoginButton = findViewById(R.id.login_with_amazon);
mLoginButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        AuthorizationManager.authorize(
                new AuthorizeRequest.Builder(requestContext)
                        .addScopes(ProfileScope.profile(), ProfileScope.postalCode())
                        .build()
        );
    }
});

这个方法在用户登录时会自动选择以下几种方式:

  1. 切换到系统浏览器,显示网页供用户输入或操作。
  2. 如果设备上安装了Amazon Shopping 电商App,会切换到安全上下文的WebView。如果用户在Amazon Shopping是已登录状态,会直接把我们的应用也登录授权,无需用户再输入或点击授权,这样就能实现单点登录了。

当我们的应用得到授权后,可以有权获取Amazon用户信息,不同的数据集称作scope。目前Login with Amazon支持以下scope:

  1. profile: 包含用户姓名,电子邮件和账号ID。
  2. profile:user_id:  仅账号ID。
  3. postal_code: 用户邮政编码。

我们上述的代码中使用addScopes(ProfileScope.profile(), ProfileScope.postalCode())声明获取了我们可以通过Login with Amazon得到的全部用户数据。

到这里,我们编译运行一下,注意因为Login with Amazon使用APK签名的APK Key来验证我们 App 的有效性,所以需要使用生成签名APK方式来构建,然后安装到模拟器中才能正常运行。还可以在build.gradle 里配置上 signingConfigs,以实现调试编译时也签名。

顺利的话,点击我们的“通过Amazon登录”按钮,模拟器中通常不会安装 Amazon Shopping,所以会跳转到浏览器,显示Amazon 登录页面。首次登录需要输入用户名和密码,之后是一个提示页,询问用户是否允许我们的应用 Cognito 访问用户数据。

获取用户信息
我们在实现AuthorizeListener的onSuccess() 方法中加上获取用户信息的方法 fetchUserProfile()。这个方法的概要定义如下:

private void fetchUserProfile() {

    User.fetch(this, new Listener<User, AuthError>() {
        /* fetch completed successfully. */
        @Override
        public void onSuccess(User user) {
            final String name = user.getUserName();
            final String email = user.getUserEmail();
            final String accountId = user.getUserId();
            final String zipCode = user.getUserPostalCode();

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    StringBuilder profileBuilder = new StringBuilder("Profile Response: ");
                    profileBuilder.append(String.format("Welcome, %s!\n", name));
                    profileBuilder.append(String.format("Your Account Id is %s\n", accountId));
                    profileBuilder.append(String.format("Your email is %s\n", email));
                    profileBuilder.append(String.format("Your zipCode is %s\n", zipCode));
                    String profile = profileBuilder.toString();
                    Toast.makeText(MainActivity.this, profile, Toast.LENGTH_LONG).show();
                }
            });
        }
        /* There was an error during the attempt to get the profile. */
        @Override
        public void onError(AuthError ae) {
     /* Retry or inform the user of the error */
        }
    });
}

User.fetch() 方法使用一个回调监听器,可以理解获取用户信息这里又是发起一次HTTPS请求,在成功响应时onSuccess(User user)这个回调方法会传入一个User对象,从这个User 对象里可以得到前面我们提到的scope 下相应的用户数据。

在应用启动时检测用户登录
用户登录进我们的应用后,关闭应用再启动应该仍可保持登录状态,仍然有权限获取Amazon用户信息。我们在当前活动的onStart()方法使用AuthorizationManager.getToken()方法来检测用户是否仍是授权状态。

@Override
protected void onStart(){
    super.onStart();
    Scope[] scopes = { ProfileScope.profile(), ProfileScope.postalCode() };
    AuthorizationManager.getToken(this, scopes, new Listener<AuthorizeResult, AuthError>() {

        @Override
        public void onSuccess(AuthorizeResult result) {
            if (result.getAccessToken() != null) {
        /* The user is signed in */
            } else {
        /* The user is not signed in */
            }
        }

        @Override
        public void onError(AuthError ae) {
    /* The user is not signed in */
        }
    });
}

由于刚才我们已经授权过了。这时关闭我们的应用再打开时,直接就弹出获取到的用户信息了。

清理授权信息注销登录
最后我们要在用户主动退出我们的应用时也把Login with Amazon的授权也注销掉。最简单的办法是放一个退出按钮,给它的onClick事件注册监听器,执行AuthorizationManager.signOut() 方法。当然还有一些额外的处理,加上界面切换登录退出状态。下面我们只列主体代码片段。

    /**
     * Sets the state of the application to reflect that the user is currently authorized.
     */
    private void setLoggedInState() {
        mLoginButton.setVisibility(Button.GONE);
        mLogoutButton.setVisibility(Button.VISIBLE);
    }

    /**
     * Sets the state of the application to reflect that the user is not currently authorized.
     */
    private void setLoggedOutState() {
        mLoginButton.setVisibility(Button.VISIBLE);
        mLogoutButton.setVisibility(Button.GONE);
    }

然后在 fetchUserProfile()方法里调用setLoggedInState(),在onCreate()方法里加一个退出按钮。

// 退出按钮,及注册点击事件
mLogoutButton = (Button) findViewById(R.id.logout);
mLogoutButton.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        // 退出 Amazon 登录
        AuthorizationManager.signOut(getApplicationContext(), new Listener<Void, AuthError>() {
            @Override
            public void onSuccess(Void response) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        setLoggedOutState();
                    }
                });
            }

            @Override
            public void onError(AuthError authError) {
                Log.e(TAG, "Error clearing authorization state.", authError);
            }
        });
    }
});

配置Amazon Cognito

下面我们来配置Amazon Cognito并关联前面我们创建的Login with Amazon的Application。

创建联合身份池
我们打开Cognito控制台(https://console.amazonaws.cn/cognito/home)

点击管理联合身份大按钮,来到联合身份页。

点击创建新的身份池按钮创建一个新的用户池,来到 创建新的身份池向导页。

身份池名称输入LoginWithAmazon。

选中启用未经验证的身份的访问权限前面的复选框。这样我们可以不登录也能访问。

在身份验证提供商下面点选Amazon,在Amazon 应用程序 ID 输入前面我们在Login With Amazon 创建的应用的App ID,比如 amzn1.application.188a56d827a7d6555a8b67a5d。

然后点击创建池按钮创建,会跳转到 Your Cognito identities require access to your resources 页。这里其实是 Cognito调用 IAM 去创建2个角色。可以点开查看详细信息查看一下详情,我们只需要点击右下角允许按钮即可。然后就来到示例代码页,表示已经创建成功。点击展开获取 AWS 凭证一段,这里显示如下的示例代码 :

// 初始化 Amazon Cognito 凭证提供程序

CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(

    getApplicationContext(),

    "cn-north-1:12345678-abcd-1234-efgh-12345678abcd", // 身份池 ID

    Regions.CN_NORTH_1 // 区域

);

配置Cognito相关的IAM角色
为了演示登录和未登录用户的不同权限,我们给授权用户赋予可以查看S3桶列表的权限。后面,我们会进一步完善我们的 App,使得授权的用户可以显示出当前S3的桶列表信息。

打开 IAM 控制台

(https://console.amazonaws.cn/iam/home)

点击左侧导航链接中的角色,可以看到刚才我们创建Cognito身份池时创建出来的2个角色:Cognito_LoginWithAmazonAuth_Role和Cognito_LoginWithAmazonUnauth_Role。顾名思义,前者是授权用户的角色,而后者是未授权用户的角色。

点击Cognito_LoginWithAmazonAuth_Role链接,编辑这个角色。在权限选项卡下点击附加策略按钮,搜索并添加AmazonS3ReadOnlyAccess这个策略。这样这个授权用户的角色就可以有S3读取相关的权限了。

把Cognito认证加入App
我们继续打开Android Studio 中 Cognito 这个项目。先把 AWS SDK 集成进项目中。使用 Gradle 管理库时,集成非常方便。我们只需要在 /app/build.gradle 中dependencies 段加上以下几行即可:

compile 'com.amazonaws:aws-android-sdk-core:2.2.22'

compile 'com.amazonaws:aws-android-sdk-cognito:2.2.22'

compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.22'

我们把前面Cognito控制台的的示例代码分离出常量和类变量,再添加到在 onCreate() 方法中。我们再添加一个getIdentity()方法,先只获取 身份ID ,验证 Cognito 已正常启用。注意获取身份ID是调用  AWS 的API,需要发起HTTP请求,按照Android开发的习惯,需要在主界面开新线程来做请求。示例代码如下:

private static final Regions MY_REGION = Regions.CN_NORTH_1;
private String identityPoolId;
private CognitoCachingCredentialsProvider credentialsProvider;

…

   private void getIdentity() {
        new Thread(){
            @Override
            public void run() {
                super.run();
                try {

    // 先只获取身份ID ,验证 Cognito 已正常启用。
                    String identityId = credentialsProvider.getIdentityId();
                    Log.d(TAG, "my ID is " + identityId);
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
        }.start();
    }


@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        identityPoolId = "cn-north-1:12345678-abcd-1234-efgh-12345678abcd";
        // 初始化 Amazon Cognito 凭证提供程序
        credentialsProvider = new CognitoCachingCredentialsProvider(
            getApplicationContext(),
            identityPoolId, // 身份池 ID
            MY_REGION // 区域
        );

    getIdentity();

…

编译调试运行一下,顺利的话,我们可以在日志输出看到

11-06 16:05:11.151 25362-25381/com.example.cognito D/CognitoLwA: my ID is cn-north-1: 12345678-abcd-1234-efgh-12345678abcd

表示Cognito已经接入成功,并认证为一个未授权用户。

这时回到Cognito的控制台,可以看到LoginWithAmazon 这个项目下的身份信息已经从以前的0变成了1。

集成Cognito 和 Login with Amazon
我们把通过Login with Amazon 登录的用户联合认证为AWS的授权用户,由Cognito赋予前述的Cognito_LoginWithAmazonAuth_Role角色。

用户首次通过Login with Amazon登录,是在onCreate() 方法里AuthorizeListener的onSuccess() 回调方法里获取的用户信息,已登录的用户关闭再打开App时是在onStart() 方法里的AuthorizationManager.getToken 的onSuccess() 回调方法里获取的用户信息。我们需要在这2处都加上集成 Cognito联合身份认证的功能。

由于用户在Login with Amazon时会跳转到浏览器,所以登录成功后再回到我们的MainActivity时都会触发onStart()方法把界面重绘。前面我们已经在onStart()方法里编写了实现用户已登录状态的代码,已经得到了Login with Amazon的验证token。我们把如下联合认证的代码示例添加到onStart()方法里,就是把Login with Amazon的验证token传递给CognitoCachingCredentialsProvider 的 setLogins()方法。

@Override

            public void onSuccess(AuthorizeResult result) {

                String token = result.getAccessToken();

                if (null != token) {

                    /* 用户已登录,联合登录Cognito*/

                    Map<String, String> logins = new HashMap<String, String>();

                    logins.put("www.amazon.com", token);

                    credentialsProvider.setLogins(logins);

                    getIdentity();

                    fetchUserProfile();

                } else {

                    /* The user is not signed in */

                }

      }

使用S3读取桶列表演示已登录和未登录用户的不同权限
我们给Cognito相关的2个IAM角色中已授权的角色赋予读取S3的权限,另一个保持不变,然后在我们的 App 里尝试读取 S3 桶列表,以分别演示已登录和未登录用户的不同权限效果。

在 IAM 控制台找到Cognito_LoginWithAmazonAuth_Role  这个角色,在其权限选项页点击附加策略按钮。找到并添加AmazonS3ReadOnlyAccess这个策略,就可以让这个角色有S3的只读权限了。

然后我们回到Android Studio 。之前我们已经有了一个 getIdentity() 方法,当时只是为了验证Cognito已经成功获取AWS账号权限。现在我们在这个方法里再加上读取S3的部分代码,把它扩展成下面这样。

private void getIdentity() {
    new Thread(){
        @Override
        public void run() {
            super.run();
            // 先只获取身份ID ,验证 Cognito 已正常启用。
            String identityId = credentialsProvider.getIdentityId();
            Log.d(TAG, "my ID is " + identityId);
            try {
                AmazonS3 s3 = new AmazonS3Client(credentialsProvider);
                s3.setRegion(Region.getRegion(MY_REGION));
                List<Bucket> bucketList = s3.listBuckets();
                final StringBuilder bucketNameList = new StringBuilder("My S3 buckets are:\n");
                for (Bucket bucket : bucketList) {
                    bucketNameList.append(bucket.getName()).append("\n");
                }
                Log.d(TAG, "s3 bucket" + bucketNameList);
                runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                      Toast.makeText(MainActivity.this, bucketNameList, Toast.LENGTH_LONG).show();
                   }
                 });
               }
               catch (Exception e) {
                   e.printStackTrace();
                   runOnUiThread(new Runnable() {
                       @Override
                       public void run() {
                        Toast.makeText(MainActivity.this, "This is OK as not authenticated to list S3 bucket.", Toast.LENGTH_LONG).show();
                       }
                   });
            }
        }
    }.start();
}

熟悉 S3 SDK 的朋友们应该不会感到陌生,这里核心就是用 AmazonS3 s3 = new AmazonS3Client(credentialsProvider); 初始化一个 s3 客户端对象实例。而传递给这个构造函数的参数,就是我们之前通过 Cognito 联合认证得到的 CognitoCachingCredentialsProvider 的实例。这其实也是 AWS 对移动应用建议的验证方式。

为退出按钮增加退出 Cognito 验证
前面我们已经做了一个退出按钮,点击时会退出Login With Amazon 的登录,我们再把退出 Cognito 验证的功能也加上,整个项目的功能就完成了。我们找到那个退出按钮注册的点击事件 mLogoutButton.setOnClickListener(),只要加一句

credentialsProvider.clearCredentials();
就行了。

小结

今天我们结合实例,为大家介绍了使用Amazon Cognito 集成 Login with Amazon 的配置及使用方法。演示了 Cognito在移动端用户认证管理方面的功能,以及开发的便捷。 AWS SDK 支持的主流移动端开发都可以接入Cognito,并且可以实现不同设备间用户状态的统一管理。希望此例可以帮助大家更多体验Cognito,便利大家移动端的开发工作。

相关资源链接

本例代码已在 github 分享:(https://github.com/xfsnow/android/tree/master/Cognito)

Login with Amazon:(http://login.amazon.com)

Amazon Cognito:(https://thinkwithwp.com/cognito)

作者介绍:

薛峰,亚马逊AWS解决方案架构师,AWS的云计算方案架构的咨询和设计,同时致力于AWS云服务在国内和全球的应用和推广,在大规模并发应用架构、移动应用以及无服务器架构等方面有丰富的实践经验。在加入AWS之前曾长期从事互联网应用开发,先后在新浪、唯品会等公司担任架构师、技术总监等职位。对跨平台多终端的互联网应用架构和方案有深入的研究。