将您的安全 OAuth2 Web 应用程序与 Android 连接

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

在我的上一篇文章中,我展示了 如何将 Android Wear 设备的自定义通知发送 到 Android Wear 手表。

为了更好地展示这一点,我使用 Teamgeist 应用程序 中的事件作为案例以及通知的外观。

我嘲笑了服务器端数据,因为重点是通知部分。今天,我想通过 Android 应用程序连接到我们的服务器端数据。因此,我需要在服务器上对 Android 用户进行身份验证。

Google OAuth2 流程如何为我们的 Web 应用程序工作

我们的 Teamgeist 应用程序使用 OAuth2 和 Google 来验证用户并获取有关用户个人资料图片或电子邮件地址的一些信息。第一次使用 OAuth2 流程可能会非常棘手。我们的 JavaScript 客户端和服务器部分之间的流程可以简化为以下步骤:

1. 从我们的 JavaScript 应用程序到服务器的第一个请求将在 HTTP 标头中不带令牌发送。因此,我们将用户重定向到显示 Google+ 登录按钮的登录页面:

2. 根据当前用户登录状态,下一页可能是登录屏幕或选择页面(例如,如果用户使用多个 Google 帐户)。假设用户登录。下一页将是我们的同意屏幕:

如您所见,我们的应用程序想要访问用户的个人资料信息。如果用户授予权限并接受此屏幕,Google 将生成一个授权码。在 OAuth2 设置期间,Google 要求提供回调 URL。生成的授权代码将发送到此 URL。

作为授权代码的交换,服务器从 Google 检索访问令牌。使用此令牌,服务器可以在一定时间内获取有关用户个人资料的信息。此外,服务器获得一个持续时间更长的刷新令牌,可用于获取新的访问令牌。

访问和刷新令牌应存储在服务器上。这些令牌永远不应提供给客户端应用程序(无论是 Android 还是 JavaScript)。在客户端,我们存储一个应用程序承载令牌。我们将此令牌关联到用户并将其提供给客户端。这是客户端需要与我们的服务器通信的唯一标记。

将 Android 连接到现有流程

假设用户使用已通过网络注册的 Android 应用程序。为了从服务器获取任何信息,例如事件或荣誉,我们必须发起对不记名令牌的请求。我们遵循了 Google 的 一篇博客文章 ,逐步介绍了跨客户端身份,这使我们找到了两个可行的解决方案。两者都有其限制。

对于这两者,您需要在 Google Developer Console 中的应用程序项目内部注册一个 Android 应用程序。不要创建一个新的,因为它们将链接在一起并且必须是同一项目的一部分。请验证两次您输入的 Android 应用程序的 SHA1 密钥和您的 Android 应用程序的程序包名称。一开始,我们通过将包名从 io.teamgeist.app 更改为 io.teamgeist.android 来重构当前的通知应用程序。这会导致令人沮丧的 INVALID_CLIENT_ID 和 INVALID_AUDIENCE 错误。当我们改回 .app 并在开发人员控制台中重新创建 Android 应用程序时,一切都开始工作了。我们没有尝试将其重命名回 .android,因此我们无法判断这是否是包名称中的禁止关键字,或者我们对 IDE 重命名 android 包名称过于自信。如果您遇到任何错误,请检查您的密钥库 SHA1 密钥和您的包名。另请查看此 博客文章 ,它非常方便。

如果一切正确,您可以从 GoogleAuthUtil 获取授权码或 GoogleIdToken。为此,您需要在您的项目中注册的服务器或 Web 应用程序的客户端 ID。

选择谷歌账户

在开始之前,您需要让用户选择一个 Google 帐户。这是通过从 AccountPicker 类调用 Choose Account Intent 来完成的:


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

当用户选择一个帐户时,将触发启动活动的 onActivityResult 方法。为了获得授权代码或 GoogleIdToken,您需要从 intent extras 中获取用户的电子邮件地址。


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

授权码

使用与授权代码相同的流程的想法很诱人。从您的 Android 应用程序获取代码并将其发送到服务器,服务器可以将其交换为一对刷新/访问令牌。您可以通过以下方式请求授权码:


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

此调用是阻塞的,因此必须在 AsyncTask 中执行。另外scope参数的server_client_id必须是服务器的client id。

当你调用它时,你不会得到一个授权代码,而是一个 UserRecoverableAuthException 类型的异常,因为你需要授权你的 Android 应用程序进行离线访问。异常本身已经包含要触发的意图。它将启动一个同意屏幕,用户必须在其中授予应用程序请求的权限。


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

当您向范围字符串添加更多范围时(请参阅 com.google.android.gms.common.Scopes 了解可用权限),同意将包含更多权限请求。

用户接受同意后,将调用启动 Activity 的 onActivityResult。从 extras 中你可以得到授权码:


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

该代码的生存时间 (TTL) 非常短,只能使用一次。将令牌发送到服务器后,您可以获得刷新和访问令牌作为交换。之后创建一个不记名令牌并将其返回到 Android 应用程序,就像您使用 JavaScript 应用程序一样。将不记名令牌添加到所有 REST 调用的标头中。

授权代码通过刷新令牌授予对应用程序的离线访问权限。这就是我们不喜欢这个解决方案的原因之一:

1. 如果用户从 Android 应用程序注销(删除 Bearer Token),您将需要重新执行所有步骤,包括同意屏幕。

2. 在我们的示例中,我们也不需要离线访问用户数据(这意味着服务器可以在没有任何用户交互的情况下与 Google 交互)。正如我们假设用户已经通过网络注册并已授予离线访问权限。

在我们的 Android 应用程序中,我们只想获取服务器中包含的数据。接下来,让我们看一下 GoogleIdToken 方法。

GoogleIdToken

GoogleIdToken 是一个 JSON Web 令牌 (JWT)。 JWT 包含三个部分:Header、payload 和 signature。签名已加密并包含标头和有效负载。使用来自 Google (https://www.googleapis.com/oauth2/v1/certs) 的公钥,每个人都可以解密签名并检查它是否与标头和有效负载相匹配。

GoogleIdToken 负载包含一些关于用户和应用程序的信息。如果您将令牌发送到 https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=[jwt],有效载荷可能如下所示:


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

在服务器上,您将验证签名,然后查看有效负载。

1.如果签名检查没问题,就知道token是谷歌创建的。

2. 你知道 Google 已经验证了你的 Android 应用程序(它检查了你的 Android 应用程序的 SHA-1 密钥和包名称,并将它们与在与 Web/服务器应用程序相同的项目中注册的 Android 客户端进行比较)并因此提供了你的带有 JWT 的应用程序,用于有效负载中具有电子邮件地址的用户。

这就是为什么您必须检查“受众”字段的原因。它必须包含您的网络/服务器应用程序客户端 ID。您还可以检查“issued_to”字段(也称为“azp”)。它包含您的 Android 应用程序的客户端 ID。但只要您只有一个客户端以这种方式与您的服务器通信,那么这并不是真正需要的。谷歌表示这个字段可以从 root 设备伪造,尽管我们不知道我们将如何实现这一点。

那么让我们回到我们的应用程序。我们想要获取 GoogleIdToken。您可以使用与获取授权代码相同的方法调用从 Android 获取它:

更改调用中的范围参数


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

与授权码请求不同,这次我们直接得到响应。不需要同意屏幕,因为用户已经授予服务器应用程序权限。在服务器端,Google 使用 GoogleIdTokenVerifier 提供令牌签名验证。您还应该通过 GoogleIdTokenVerifier 的生成器提供 Web/服务器应用程序的客户端 ID:


 Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

您验证了用户和应用程序,并可以将 Bearer Token 发送到 Android 应用程序,如前所述,使用授权码。您额外获得的好处是:

1. 您无需额外调用 Google 来验证令牌。

2. 您甚至可以更改您的服务器应用程序,使其不仅接受不记名令牌,还接受 GoogleIdToken。因此,您可以省去创建不记名令牌并将其存储到数据库中。

您唯一需要做的就是检查用户是否已经从 Web 登录,并使用来自 JWT 的社交 ID 或电子邮件地址在您的用户数据库中搜索用户数据。

缺点:

1. 用户必须已经通过来自 webapp 的授权代码流登录到应用程序。用户必须在那里接受同意屏幕。

2. 用户永远不会注销。如果 JWT 过期(60 分钟),Android 应用程序无需与您的服务器交互即可获得新的 JWT。即使您使用户的所有 Bearer Token 失效,JWT 仍然有效。只能通过向数据库中的用户添加标志来阻止用户。

3. 您无法使用 JWT 从 Google 访问服务器端的其他数据。

4. 除了 Bearer Token 之外,检查 JWT 需要在我们的服务器端进行更改。

除了所有缺点之外,我们更喜欢 JWT 方法。一个建议是创建这两种可能性的混合体。使用授权码进行用户注册并获取访问/刷新令牌。对于用户标识,仅使用 GoogleIdToken。

在我们的下一集中,我们将使用登录从我们的服务器定期收集事件并将它们作为通知推送到 Android Wear 智能手表。

请随时在下面的评论部分分享您的想法和意见。