在上一节中,你启动了一个Android项目,该项目使用Auth0进行用户登录、注销,以及读取和更新用户元数据。你在Auth0和应用程序两端都设置了该项目。在本节中,你将完成该项目,并对其进行更新,以确保其在纵向和横向方向上都能工作。
编写代码
到目前为止,你在这个练习中所做的一切只是一个前奏。现在是编写实际代码的时候了!这是练习中最大的任务,所以让我们分小步来完成它。
🛠移动到 app/java/com.example.login文件夹,打开主活动的文件。 MainActivity.kt.它的内容应该是这样的。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
导入必要的库
🛠 在文件中已存在的语句中添加以下内容 import语句添加到文件中的语句。
import androidx.core.view.isVisible
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.management.ManagementException
import com.auth0.android.management.UsersAPIClient
import com.auth0.android.provider.WebAuthProvider
import com.auth0.android.result.Credentials
import com.auth0.android.result.UserProfile
import com.google.android.material.snackbar.Snackbar
这些语句中的大部分 import语句从Auth0的库中导入类。下面是这些类的作用。
AuthenticationAPIClient:访问Auth0认证API。该应用程序使用它来检索用户的个人资料信息。AuthenticationException:定义认证过程中可能出现的错误和异常。Callback::定义了一个包含 "成功 "和 "失败 "回调函数的对象,Auth0在完成一个API函数后应调用该函数。ManagementException:: 定义了Callback对象可能需要处理的错误和异常。UsersAPIClient:管理用户信息。应用程序使用它来检索用户的资料信息。WebAuthProvider:为应用程序提供Auth0的基于网页的登录。应用程序使用它来通过网页浏览器的登录页面登录用户,并将用户退出。Credentials:存储用户的凭证,包括ID、访问和刷新令牌。UserProfile:存储用户的个人资料信息,包括他们的用户ID、姓名、电子邮件地址和元数据。
🛠 你还需要导入Activity 's auto-generated view binding library,这将使你的代码能够在布局中引用视图(或者更简单地说:访问屏幕上的小部件)。做到这一点,需要添加以下 import语句,将 {YOUR_PACKAGE_NAME_HERE}用你的应用程序的包的名字,你可以在文件的第一行找到这个名字。
import {YOUR_PACKAGE_NAME_HERE}.databinding.ActivityMainBinding
我的应用程序的包名是 com.example.login,所以我的 import语句看起来像这样。
import com.example.login.databinding.ActivityMainBinding
添加类属性
🛠 将以下属性添加到MainActivity ,使类的开头看起来像这样。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// Login/logout-related properties
private lateinit var account: Auth0
private var cachedCredentials: Credentials? = null
private var cachedUserProfile: UserProfile? = null
override fun onCreate(savedInstanceState: Bundle?) {
...
你将使用第一个属性,binding ,来访问屏幕上的小部件。其他三个属性与登录和退出有关,下面将详细介绍:
account:代表应用程序的Auth0账户,并使用应用程序的客户端ID和应用程序的Auth0租户的域来实例化。让我说清楚:这不是试图登录的用户的账户,而是将登录/注销过程委托给Auth0的开发者或组织的账户。在这个例子中,它是你的Auth0开发者账户。account的值被设置在onCreate()当活动被实例化的时候,它的值被设置在cachedCredentials它包含用户的证书,在成功登录后从Auth0返回。它的值应该是null当用户没有登录时,它的值应该是。当用户登录时,它应该引用Credentials的一个实例。一个Credentials实例有以下属性。cachedUserProfile:持有用户的个人资料信息。它的值应该是null当用户没有登录时,它的值应该是。当用户登录时,它应该引用UserProfile的一个实例。一个UserProfile实例有以下属性。email:与用户账户对应的电子邮件地址。isEmailVerified:true如果用户在自己注册为用户后回复了Auth0发送的验证邮件。name:用户的全名。givenName:用户的名字,通常被称为 "名 "或 "前名"。familyName:用户的姓氏,通常被称为他们的 "姓 "或者 "surname"。nickname:用户的昵称,有时被称为他们的 "熟名 "或 "单名"。PictureURL:可以检索到用户照片的URL。createdAt:用户账户的创建日期和时间。
更新 onCreate()方法。
首先,让我们填写 onCreate()方法,并使用它来初始化活动中的一切。
🛠更新该 onCreate()方法,使它看起来像这样:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = Auth0(
getString(R.string.com_auth0_client_id),
getString(R.string.com_auth0_domain)
)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.buttonLogin.setOnClickListener { login() }
binding.buttonLogout.setOnClickListener { logout() }
binding.buttonGet.setOnClickListener { getUserMetadata() }
binding.buttonSet.setOnClickListener { setUserMetadata() }
}
这个方法:
- 定义账户对象,它包含连接到你的Auth0账户的必要凭证。
- 创建一个视图绑定对象,你将用它来访问屏幕上的小部件。
- 将登录、注销、获取和设置按钮连接到它们在被点击时应该调用的方法。
添加 login()方法。
下一步是实现当用户点击登录按钮时被调用的方法。
🛠 在类的后面添加这个方法 onCreate():
private fun login() {
WebAuthProvider
.login(account)
.withScheme(getString(R.string.com_auth0_scheme))
.withScope(getString(R.string.login_scopes))
.withAudience(getString(R.string.login_audience, getString(R.string.com_auth0_domain)))
.start(this, object : Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
showSnackBar(getString(R.string.login_failure_message, exception.getCode()))
}
override fun onSuccess(credentials: Credentials) {
cachedCredentials = credentials
showSnackBar(getString(R.string.login_success_message, credentials.accessToken))
updateUI()
showUserProfile()
}
})
}
login()使用Auth0 SDK的WebAuthProvider 类,它使应用程序能够使用Auth0的认证服务。你最常使用的WebAuthProvider 方法是其 login()和 logout()方法。
虽然这个方法的格式是跨越几行,但它只是一行代码。这一行是由对WebAuthProvider 's方法链的调用组成的,开头是 login().如果你忽略所有的注释和参数,这个方法链看起来像这样。
WebAuthProvider
.login()
.withScheme()
.withScope()
.withAudience()
.start()
这就是Builder设计模式的作用。从 login()到 withAudience(),链中的每个方法都接受一个参数,提供关于登录的额外信息,利用这些信息创建一个WebAuthProvider 对象,并将其传递给链中的下一个方法。链中的最后一个方法。 start(),将生成的WebAuthProvider 对象作为其参数,并使用它来显示登录页面,并定义当登录成功和失败时应该发生什么。
让我们来看看链中的每个方法都做什么。
login()启动登录过程并指定应用程序使用的Auth0账户。
withScheme()指定Auth0在成功登录后重定向到的URL所使用的方案。对于Web应用程序,该方案是http 或https 。该值对于本地移动应用程序是任意的,所以我们使用app ,以使其他开发人员和其他可能使用该应用程序的Auth0设置的人清楚地知道重定向不是到一个网页。
withScope()指定在用户成功登录的情况下,该应用被授权使用哪几组用户数据。Auth0的认证和授权所基于的OpenID Connect和OAuth框架,使用术语 范围来表示访问用户数据和资源的授权。该方法将一个以空格分隔的字符串作为其参数,其中字符串中的每个 "字 "都指定了不同的范围。本应用程序中使用的字符串包含这些作用域。
openid:表示使用OpenID Connect进行认证的应用程序。这是唯一需要的范围;所有其他范围是可选的。profile:授权应用程序访问基本的用户资料信息,包括名字、姓氏、昵称、他们的照片或头像,等等。email:授权应用程序访问用户的电子邮件地址。read:current_user:授权应用程序以只读的方式访问current_user的要求。update:current_user_metadata:授权应用程序对current_user_metadata要求进行读和写的访问。这个范围允许我们在用户的元数据中获取和设置country值。
withAudience()指定应用程序将用于连接到Auth0的登录服务的URL。这个URL是使用应用程序使用的Auth0租户的域和Auth0认证API的端点构建的。
start()读取由链中所有先前方法构建的WebAuthProvider 对象,并打开浏览器窗口以显示登录页面。它需要两个参数:一个上下文(对启动浏览器窗口的Activity 的引用)和一个有两个回调方法的匿名对象。
onFailure():定义了如果用户没有成功登录就从浏览器的登录界面返回,应该发生什么。这通常发生在用户关闭浏览器登录屏幕或在该屏幕上点击 "返回 "按钮时。应用程序会显示一个SnackBar,通知用户登录失败,后面有一个错误代码。onSuccess():定义了如果用户在成功登录后从浏览器登录屏幕返回,应该发生什么。应用程序处理成功的响应,显示一个SnackBar,通知用户登录成功,并将用户界面更新为 "已登录 "状态。
添加 logout()方法
你可能已经猜到了,如果有一个 login()方法在用户按下登录按钮时被调用,那么也一定有一个 logout()当用户按下注销按钮时,也会有一个方法被调用。
🛠 将这个方法添加到类的后面 login():
private fun logout() {
WebAuthProvider
.logout(account)
.withScheme(getString(R.string.com_auth0_scheme))
.start(this, object : Callback<Void?, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
updateUI()
showSnackBar(getString(R.string.general_failure_with_exception_code,
exception.getCode()))
}
override fun onSuccess(payload: Void?) {
cachedCredentials = null
cachedUserProfile = null
updateUI()
}
})
}
如同 login(), logout()也使用了Auth0 SDK的WebAuthProvider 类,并且是一个使用Builder模式的单行代码。这一次,这一行调用了一个较短的WebAuthProvider 's方法链,以 logout().如果你忽略了所有的参数,这个方法链看起来像这样。
WebAuthProvider
.logout()
.withScheme()
.start()
logout()启动注销过程,并指定应用程序使用的Auth0账户,该账户应与用于登录的账户相同。
withScheme()指定Auth0在成功注销后重定向到的URL所使用的方案。这应该与登录时使用的方案相同。
start()采取由链中所有先前的方法构建的WebAuthProvider 对象来注销用户。它需要两个参数:一个上下文(对启动注销过程的Activity 的引用)和一个有两个回调方法的匿名对象。
onFailure():定义了当注销过程失败时应该发生什么。这种情况很少发生,通常表明是网络或服务器问题。在这个例子中,应用程序更新用户界面(保持在 "登录 "状态),并显示一个SnackBar,通知用户注销失败,后面是一个错误代码。onSuccess():定义当注销过程成功时应该发生什么。在这个例子中,应用程序销毁了用户的证书和个人资料的本地副本,并将用户界面更新为 "注销 "状态。
添加 showUserProfile()方法
每个Auth0用户都有一个与他们的账户相关的用户资料。用户资料包含以下关于用户的基本信息。
- 姓名。用户的全名、名、姓和绰号
- 电子邮件信息。用户的电子邮件地址,以及它是否经过验证
- 图片。识别用户的图片的位置
- 创建日期:用户账户的创建日期和时间。
当用户成功登录时,应用程序应该在屏幕上显示他们的名字和电子邮件。它通过调用 showUserProfile()在登录成功后立即调用
🛠 将此方法添加到类的后面 logout():
private fun showUserProfile() {
// Guard against showing the profile when no user is logged in
if (cachedCredentials == null) {
return
}
val client = AuthenticationAPIClient(account)
client
.userInfo(cachedCredentials!!.accessToken!!)
.start(object : Callback<UserProfile, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
showSnackBar(getString(R.string.general_failure_with_exception_code,
exception.getCode()))
}
override fun onSuccess(profile: UserProfile) {
cachedUserProfile = profile
updateUI()
}
})
}
这个方法是由 onSuccess()中的回调方法执行的最后一项任务。 loginWithBrowser().它初始化cachedUserProfile 属性,该属性包含用户的个人资料信息。
作为预防措施,如果cachedCredentials 属性是,它立即返回。 null,这意味着没有用户登录,因此没有任何用户资料可以显示。
为了获得这些信息,它做了以下工作。
- 它创建了一个
AuthenticationAPIClient的实例,检索Auth0账户信息。像登录和注销方法一样,这也使用了Builder模式。 - 它使用
AuthenticationAPIClient'suserInfo()方法来指定我们要从Auth0检索用户的资料信息。这个方法需要一个有效的访问令牌,它从cachedCredentials属性中提取。 - 最后,它为从Auth0检索用户资料信息的失败和成功的情况定义了回调方法。如果检索成功,配置文件信息将被存储在
cachedUserProfile,用户界面将被更新以显示用户的姓名和电子邮件地址。
添加 getUserMetadata()和 setUserMetadata()方法
用户资料包含的信息通常适用于每个用户账户,无论其用于何种类型的应用--姓名、电子邮件、照片和创建日期/时间。虽然这是必要的信息,但这可能不是你想在他们的资料中存储的所有用户信息。
这就是用户元数据的作用。可以把它看作是一个键值存储,你可以把用户资料中没有涵盖的其他用户信息放在这里。在这个应用程序中,用户元数据将只存储一个额外的用户信息--他们的国家,它将允许用户检索和更新这个信息。
🛠 在类的后面添加以下内容 showUserProfile():
private fun getUserMetadata() {
// Guard against getting the metadata when no user is logged in
if (cachedCredentials == null) {
return
}
val usersClient = UsersAPIClient(account, cachedCredentials!!.accessToken!!)
usersClient
.getProfile(cachedUserProfile!!.getId()!!)
.start(object : Callback<UserProfile, ManagementException> {
override fun onFailure(exception: ManagementException) {
showSnackBar(getString(R.string.general_failure_with_exception_code,
exception.getCode()))
}
override fun onSuccess(userProfile: UserProfile) {
cachedUserProfile = userProfile
updateUI()
val country = userProfile.getUserMetadata()["country"] as String?
binding.edittextCountry.setText(country)
}
})
}
private fun setUserMetadata() {
// Guard against getting the metadata when no user is logged in
if (cachedCredentials == null) {
return
}
val usersClient = UsersAPIClient(account, cachedCredentials!!.accessToken!!)
val metadata = mapOf("country" to binding.edittextCountry.text.toString())
usersClient
.updateMetadata(cachedUserProfile!!.getId()!!, metadata)
.start(object : Callback<UserProfile, ManagementException> {
override fun onFailure(exception: ManagementException) {
showSnackBar(getString(R.string.general_failure_with_exception_code,
exception.getCode()))
}
override fun onSuccess(profile: UserProfile) {
cachedUserProfile = profile
updateUI()
showSnackBar(getString(R.string.general_success_message))
}
})
}
虽然 showUserProfile()使用AuthenticationAPIClient 的一个实例来获取用户资料信息。 getUserMetadata()和 setUserMetadata()使用一个不同的对象类型:UsersAPIClient 。
不像AuthenticationAPIClient ,它只需要一个Auth0 账户对象来实例化,你需要一个Auth0 账户对象和一个访问令牌来实例化一个UsersAPIClient 对象。
getUserMetadata()使用UsersAPIClient '的方法指定用户配置文件和用户的ID。 getProfile()方法和用户的ID,然后用 start()方法来尝试获取用户资料,并为失败和成功定义回调方法。
getUserMetadata()'s onSuccess()的回调方法几乎与 showUserProfile()'的回调方法几乎一样--它只是多了这两行,从用户的元数据中提取country ,并在屏幕上显示。
val country = userProfile.getUserMetadata()["country"] as String?
binding.edittextCountry.setText(country)
setUserMetadata()定义一个带有单个键值对的Map ,其中键值是字符串country ,相应的值是用户输入国家名称的EditText 的内容。然后,它把这个Map 和用户的ID一起传给UsersAPIClient 's updateMetadata()方法来指定要做的改变,然后用 start()方法来启动更新,并为失败和成功定义回调方法。
setUserMetadata()'s onSuccess()的回调方法几乎与 getUserMetadata()'的回调方法几乎相同--但是它并不更新用户输入国家名称的EditText ,而是简单地显示一个SnackBar ,通知用户它成功地更新了元数据。
添加UI方法
最后一步是添加向用户展示信息的方法。
🛠 在类的后面添加以下内容 getUserMetadata()和 setUserMetadata():
private fun updateUI() {
val isLoggedIn = cachedCredentials != null
binding.textviewTitle.text = if (isLoggedIn) {
getString(R.string.logged_in_title)
} else {
getString(R.string.logged_out_title)
}
binding.buttonLogin.isEnabled = !isLoggedIn
binding.buttonLogout.isEnabled = isLoggedIn
binding.linearlayoutMetadata.isVisible = isLoggedIn
binding.textviewUserProfile.isVisible = isLoggedIn
val userName = cachedUserProfile?.name ?: ""
val userEmail = cachedUserProfile?.email ?: ""
binding.textviewUserProfile.text = getString(R.string.user_profile, userName, userEmail)
if (!isLoggedIn) {
binding.edittextCountry.setText("")
}
}
private fun showSnackBar(text: String) {
Snackbar
.make(
binding.root,
text,
Snackbar.LENGTH_LONG
).show()
}
如果你已经走到了这一步,我有个好消息要告诉你:你已经写好了这个应用程序的所有代码
查看应用程序的运行情况
运行该应用程序。你会看到这个。

点击登录按钮。该应用程序将打开一个浏览器窗口,显示登录网页。

使用你之前创建的用户账户的电子邮件地址和密码登录。由于这是该账户第一次登录该应用程序,浏览器窗口中会出现授权应用程序页面。

这个页面是要求你授权应用程序访问你的用户账户中的以下信息。
- 资料:访问你的资料和电子邮件
- 当前用户:读取你的当前用户
- 当前用户数据:更新你的当前用户数据
如果这些项目听起来很熟悉,那是因为你最近见过它们。它们是login_scopes 字符串中的作用域的名称,在 strings.xml资源中的范围名称...
<string name="login_scopes">
openid profile email read:current_user update:current_user_metadata
</string>
...其中调用 withScope()方法中的 login()方法中的调用,用于指定应用程序被授权使用的用户数据集。这个额外的步骤通知用户对其数据的这种使用,并让他们选择批准或拒绝。
点批准按钮。浏览器将消失,你将返回到应用程序,现在看起来像这样。

点选 "获取"按钮。如果你从未编辑过用户元数据中country 字段的值,元数据文本框将显示提示文本 "输入国家"。
在文本框中输入一个国家的名称(或者任何其他文本,如果你喜欢的话),然后点击设置按钮。你会看到这个。

刚刚发生了两件事。
- 如果用户的元数据已经有一个
country字段,它的值就会变成你输入的任何文本。如果用户的元数据没有country字段,则在元数据中添加一个country字段,其值被设置为你输入的任何文本。 - 一个名为 "A "的安卓弹出式信息出现在屏幕底部。
Snackbar的弹出信息出现在屏幕底部,信息是 "成功!"
退出应用程序,然后再次登录。点选 "获取"按钮。该应用程序应该用country 元数据字段的值来填充元数据文本框。
让我们从Auth0方面看一下更新的用户元数据。在Auth0的仪表板上,进入用户列表。通过点击页面左侧菜单中的用户管理,然后点击用户。选择当前在应用程序上登录的用户。你将会被带到该用户的详细信息页面。

向下滚动到该页面的元数据部分。你会看到一个标有user_metadata的区域。如果你在应用程序中填写了EditText ,你会在那里的JSON对象中看到它。
{
"country": {WHATEVER_TEXT_YOU_ENTERED}
}
每个Auth0用户账户可以存储两种元数据:
- 用户元数据:这是指由用户有意提供并控制的数据。它通常用于存储用户希望与应用程序分享的信息,如他们的地址、联系信息、偏好和类似数据。只有用户可以阅读和编辑的数据应该被存储在这里。
- 应用元数据:是指由应用提供和控制的数据。它通常用于存储关于用户的信息,这些信息主要是供应用程序使用的,如用户的角色、权限、状态和类似的数据。可能会有用户可以读取这些数据的情况,但你不应该使用应用元数据来存储用户直接提供的数据。
虽然对用户账户页面的详细研究超出了本文的范围,但你应该探索它们,看看与用户账户相关的各类数据和功能。
当事情走偏时
"走偏 "的意思是 "出错 "或 "不按计划进行"。在本节中,我将向你展示,当你在编写安卓应用时,如果没有考虑到用户将手机翻转过来时的情况,会出现什么问题。
诱发应用程序失忆症
运行该应用程序并登录。你应该看到这个:

将你的设备旋转到横向方向:

注意发生了什么:
- 问候语从 "你已经登录了。"变成了 "欢迎来到这个应用程序!",这是该应用程序启动时的状态。
- 按钮也回到了它们的初始状态。 登录被启用,注销被禁用。
看起来好像是把你的设备翻过来,让你的应用程序失忆,回到了它的初始状态。发生了什么?
配置改变和重新加载
用一个开发者的陈词滥调来说:这不是一个错误,而是一个功能。
每当检测到配置变化时,Android的默认响应是重新加载当前活动。有很多配置变化,比如语言、键盘的可用性,或者你刚才的变化:屏幕方向。当配置发生变化时,重新加载一个活动会使它再次运行其初始化方法并重新加载资源,使它能够为新的配置进行自我设置。
这种行为是为那些在不同方向上呈现不同界面的应用程序设计的。YouTube就是这样一个应用的例子,它有不同的纵向和横向UI设置。

改变屏幕方向可以重新加载活动,并将应用程序恢复到初始状态,但它并没有改变你的 "登录 "状态。该 logout()方法并没有在你把设备转过来的时候被调用。你仍然是登录状态!
你可以通过点击登录按钮确认你的 "登录 "状态。请注意,没有一个中间步骤,你必须输入你的电子邮件地址和密码--你被直接带到了应用程序。

请注意,该应用程序从未出现过登录页面。那是因为没有任何必要--你仍然在登录。让我们来解决这个问题。
治愈你的应用程序的失忆症
你可以指示安卓系统在应用程序清单中的特定配置变化时不重新加载活动。
🛠 打开 AndroidManifest.xml并将 <activity>标签改为以下内容:
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize" >
这一改动将属性 android:configChanges= “orientation|screenSize”到MainActivity ,它告诉Android,如果设备方向或屏幕尺寸发生变化,该活动不应该重新加载。Android使用一个回调方法来通知应用程序的变化,而不是重新加载活动。假设你会自己处理这些配置变化。因为我们会忽略这个回调,所以配置变化没有影响。
运行应用程序并登录。改变屏幕方向,从纵向到横向,然后再返回。它不再导致活动的重新加载。
总结
你刚刚建立了一个简单的应用程序,它具有基本的用户名/密码验证功能--能够识别一个已知的用户。除了记录一个用户的进出,你还可以检索他们档案中的信息,并读取和更新他们的元数据。
你可以在 Auth0博客样本GitHub账户的这个仓库中找到本文的完整项目代码。为了运行它,你需要做的唯一改变是在资源文件中输入你的应用程序的客户端ID和租户的域名。 auth0.xml资源文件。
这是关于使用Auth0进行Android开发的新系列文章中的第一篇。未来的文章将涵盖Android 12和Android Studio的新发展,以及对Android和Auth0的认证和授权以及标准的用户名和密码方法的替代方案进行更深入的探讨。请关注这个空间。