跳转至

Android SDK

本指南介绍如何将 Authany 集成到 Android 应用中。支持的平台包括:

  • Android 5.0(API 21)及以上版本

按照本指南,只需 🕐 10 分钟即可将 Authany 添加到您的 Android 应用中。

说明

本教程演示应用的完整代码可以在这个 GitHub 仓库中找到。

在 Authany 中设置应用

在 https://manage.authany.com 注册一个 Authany 管理台账号,或者使用您自建部署的 Authany。

在项目列表中,创建一个新项目或选择一个已有项目。接下来,我们需要在该项目中创建一个应用。

步骤 1:在管理台中创建应用

在左侧菜单栏中进入 应用。

在顶部工具栏中点击 ⊕新增应用。

输入应用名称,并将应用类型选择为 Native App。点击「保存」。

您会看到一份可以帮助您完成设置的指南列表,然后点击「下一步」。

创建应用

步骤 2:配置应用

定义一个自定义 URI scheme,用户完成身份验证后,Authany 会用它把用户重定向回您的应用。该 scheme 应基于您 Android 应用的包名。对于本指南中创建的演示应用,scheme 为:com.example.authgeardemo://host/path。想了解更多关于在 Android 中设置自定义 URI scheme 的内容,请查看官方文档。

返回 Authany 管理台,将您定义的 URL scheme 添加为重定向 URI。对于我们的演示应用,请添加以下 URI:

com.example.authgeardemo://host/path

在顶部工具栏点击「保存」,并记下 Client ID,后续在 Android 应用中会用到。您也可以之后在应用列表中重新获取它。

填写重定向 URI

将 Authany 添加到 Android 应用

在这一步中,我们会使用前面步骤中设置好的 Authany 客户端应用,为 Android 应用添加用户身份验证。

Authany SDK 同时支持 Jetpack Compose 和传统的 View/XML UI 工具包。下面的步骤 4分别提供了两种实现方式,请选择与您应用匹配的标签页。

前置条件

跟着本指南操作,您需要准备以下内容:

  • 电脑上已安装 Android Studio
  • 具备 Kotlin 或 Java 的基础知识

步骤 1:创建 Android 应用项目

本指南将创建一个简单的新 Android 应用项目。如果您是要把 Authany 添加到已有应用中,可以跳过这一步。

打开 Android Studio,按以下信息创建一个新项目:

  • 在 Activity 选择界面,选择 Empty Activity(Jetpack Compose)或 Empty Views Activity(XML),本指南两种都会介绍。Android Studio 当前默认使用的 Empty Activity 采用的是 Jetpack Compose。
  • 名称:My Demo App
  • 构建配置语言:Groovy DSL

说明

本指南建议将构建配置语言设置为 Groovy DSL,这样可以更方便地直接复制粘贴我们提供的 Gradle 配置,而不需要做太多改写。

步骤 2:将 Authany SDK 添加到项目中

Authany Android SDK 让您的 Android 应用更方便地与 Authany 的端点和服务交互。

该 SDK 发布在 Maven Central 上。请确保项目中已启用 mavenCentral() 仓库。新建的 Android Studio 项目默认已包含该仓库;如果没有,请将其添加到项目的 settings.gradle 文件中:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
    }
}

接下来,在应用级(/app/build.gradle)的 build.gradle 文件的 dependencies 部分添加 Authany SDK:

dependencies {
    // Other implementations
    implementation 'com.authgear:android-sdk:3.0.0'
}

撰写本文时,3.0.0 是最新版本。您可以在 Maven Central 或 release 标签中查看是否有更新版本。

启用 Java 8+ API desugaring 支持

要为项目启用 Java 8+ API desugaring 支持,请对应用级的 build.gradle 文件做以下修改。

  1. 在 android > compileOptions 部分中添加 coreLibraryDesugaringEnabled true:
compileOptions {
    coreLibraryDesugaringEnabled true
}
  1. 然后在 dependencies 部分添加 coreLibraryDesugaring:
dependencies {
    // Other implementations
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
}

想了解更多关于 Java 8+ API desugaring 支持的内容,请查看这里。

注意

您应用的窗口主题(window theme)必须继承自 Theme.AppCompat 主题。SDK 的身份验证界面(OAuthActivity、OAuthRedirectActivity)都是 AppCompatActivity,如果应用主题是普通的框架主题,登录时 Android 会抛出 You need to use a Theme.AppCompat theme (or descendant) with this activity 错误。

Empty Views Activity 项目默认已经使用了 Material Components(AppCompat 的子类)主题。如果您创建的是 Jetpack Compose 项目,请打开 res/values/themes.xml,确保应用主题的父主题是 AppCompat 或 Material 3,例如:

<resources>
    <style name="Theme.MyDemoApp" parent="Theme.AppCompat.DayNight.NoActionBar" />
</resources>

您的 Compose UI 仍然会有自己的主题;这里设置的只是窗口主题。

同步 Gradle 后继续。

步骤 3:设置重定向 URI

在应用的 AndroidManifest.xml 中添加以下 activity 条目。intent 系统会把重定向 URI 分发给 OAuthRedirectActivity,之后的处理由 SDK 完成。

<!-- 应用的其他配置内容,这里为简洁起见省略 -->
<application>
    <!-- 其他 activity 或条目 -->

    <!-- 添加以下 activity -->
    <!-- 需要设置 android:exported="true" -->
    <!-- 参见 https://developer.android.com/about/versions/12/behavior-changes-12#exported -->
    <activity android:name="com.oursky.authgear.OAuthRedirectActivity"
        android:exported="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" />
            <!-- 将 data 配置为您应用实际使用的重定向 URI -->
            <!-- 这里使用的是在管理台中配置的 com.example.authgeardemo://host/path -->
            <!-- 注意:AuthenticateOptions 中提供的 redirectURI 也必须与此匹配 -->
            <data android:scheme="com.example.authgeardemo"
                android:host="host"
                android:pathPrefix="/path"/>
        </intent-filter>
    </activity>
</application>

面向 API level 30 及以上(Android 11 及以上)

如果您的 Android 应用面向 API level 30 及以上(Android 11 及以上),需要在 AndroidManifest.xml 中添加一个 queries 部分。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <!-- Other elements such <application> -->
  <queries>
    <intent>
      <action android:name="android.support.customtabs.action.CustomTabsService" />
    </intent>
  </queries>
</manifest>

步骤 4:实现身份验证

现在初始化 Authany,并搭建包含 Login 按钮的界面,同时为已登录用户提供 User Settings 和 Logout。请选择与您 UI 工具包匹配的标签页,两者最终实现的流程相同。

请将下面代码中的 <CLIENT_ID> 和 <AUTHGEAR_ENDPOINT> 替换为您 Authany 客户端应用配置页面中的对应值。

SDK 暴露的是 suspend 函数,因此您需要在 Compose 中通过 rememberCoroutineScope() 和 LaunchedEffect 来调用它们。

在 MainActivity 中创建 Authany 实例,并承载您的 Compose UI:

class MainActivity : ComponentActivity() {

    private lateinit var authgear: Authgear

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        authgear = Authgear(application, "<CLIENT_ID>", "<AUTHGEAR_ENDPOINT>")
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    MainScreen(authgear)
                }
            }
        }
    }
}

MainScreen 这个 composable 会在首次出现时配置 Authany(恢复已有会话),然后根据 sessionState 显示 Login 按钮或已登录视图:

private const val REDIRECT_URI = "com.example.authgeardemo://host/path"
private const val TAG = "AuthgearDemo"

@Composable
fun MainScreen(authgear: Authgear) {
    val scope = rememberCoroutineScope()
    var configuring by remember { mutableStateOf(true) }
    var email by remember { mutableStateOf<String?>(null) }
    var busy by remember { mutableStateOf(false) }

    // Configure once when the screen first appears; restore an existing session.
    LaunchedEffect(Unit) {
        try {
            authgear.configure()
            if (authgear.sessionState == SessionState.AUTHENTICATED) {
                email = authgear.fetchUserInfo().email
            }
        } catch (e: Throwable) {
            Log.e(TAG, "configure failed", e)
        }
        configuring = false
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        when {
            configuring -> CircularProgressIndicator()

            email != null -> {
                Text("Welcome, $email")
                OutlinedButton(
                    enabled = !busy,
                    onClick = { authgear.open(Page.SETTINGS) },
                    modifier = Modifier.padding(top = 16.dp),
                ) { Text("User Settings") }
                Button(
                    enabled = !busy,
                    onClick = {
                        scope.launch {
                            busy = true
                            try {
                                authgear.logout()
                                email = null
                            } catch (e: Throwable) {
                                Log.e(TAG, "logout failed", e)
                            }
                            busy = false
                        }
                    },
                ) { Text("Logout") }
            }

            else -> {
                Button(
                    enabled = !busy,
                    onClick = {
                        scope.launch {
                            busy = true
                            try {
                                email = authgear.authenticate(AuthenticateOptions(REDIRECT_URI)).email
                            } catch (e: Throwable) {
                                Log.e(TAG, "login failed", e)
                            }
                            busy = false
                        }
                    },
                ) { Text("Login") }
            }
        }
    }
}

说明

configure、authenticate、logout 和 fetchUserInfo 都是 Authgear 上的 suspend 扩展函数,如果显示无法解析,请导入它们(例如 import com.oursky.authgear.configure)。其他无法解析的类也请一并导入。

启用 View Binding

在应用级(/app/build.gradle)的 build.gradle 文件中,向 android 代码块下添加以下内容:

buildFeatures {
    viewBinding = true
}

搭建布局

打开 res/layout/activity_main.xml,删除默认的「Hello World!」TextView,添加一个标题、一个 Login 按钮,以及登录后显示的视图(进度条、欢迎文字、User Settings 和 Logout 按钮),并将它们分组以便统一显示或隐藏:

<TextView
    android:id="@+id/app_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="My Demo App!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/login_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Login"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/app_title" />

<ProgressBar
    android:id="@+id/progressBar"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="48dp"
    android:indeterminate="true"
    android:visibility="invisible"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<TextView
    android:id="@+id/welcome_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="welcome user"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/login_btn" />

<Button
    android:id="@+id/user_settings_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="User Settings"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/welcome_text" />

<Button
    android:id="@+id/logout_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Logout"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/user_settings_btn" />

<androidx.constraintlayout.widget.Group
    android:id="@+id/logged_in_views"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    app:constraint_referenced_ids="welcome_text,user_settings_btn,logout_btn" />

完整的 activity_main.xml 文件可以在这里查看。

初始化 Authany 并绑定按钮

在 MainActivity.kt 中,使用 view binding 初始化 Authany,调用 configure(),并连接各个按钮:

class MainActivity : AppCompatActivity() {

    private lateinit var authgear: Authgear
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        authgear = Authgear(application, "<CLIENT_ID>", "<AUTHGEAR_ENDPOINT>")
        authgear.configure(object : OnConfigureListener {
            override fun onConfigured() {
                updateUi(authgear)
            }

            override fun onConfigurationFailed(throwable: Throwable) {
                Log.d("TAG", throwable.toString())
                // Something went wrong, check the client ID or endpoint.
            }
        })

        binding.loginBtn.setOnClickListener { startLogin() }
        binding.logoutBtn.setOnClickListener { logout() }
        binding.userSettingsBtn.setOnClickListener { openUserSettings() }
    }
}

实现具体操作

在 MainActivity 中添加以下方法。startLogin() 用于启动身份验证流程,updateUi() 用于反映会话状态(并获取用户邮箱),logout() 用于结束会话,openUserSettings() 用于打开预置的设置页面:

fun startLogin() {
    binding.progressBar.visibility = View.VISIBLE
    val options = AuthenticateOptions("com.example.authgeardemo://host/path")
    authgear.authenticate(options, object : OnAuthenticateListener {
        override fun onAuthenticated(userInfo: UserInfo) {
            updateUi(authgear)
        }

        override fun onAuthenticationFailed(throwable: Throwable) {
            binding.progressBar.visibility = View.INVISIBLE
            Log.d("TAG", throwable.toString())
        }
    })
}

fun updateUi(authgear: Authgear) {
    val state = authgear.sessionState
    if (state == SessionState.AUTHENTICATED) {
        binding.loginBtn.visibility = View.GONE
        binding.loggedInViews.visibility = View.VISIBLE
        // Get userInfo and display in welcome text
        authgear.fetchUserInfo(object : OnFetchUserInfoListener {
            override fun onFetchedUserInfo(userInfo: UserInfo) {
                binding.welcomeText.text = userInfo.email
            }

            override fun onFetchingUserInfoFailed(throwable: Throwable) {
                Log.d("TAG", "Failed to fetch UserInfo")
            }
        })
    } else {
        binding.loggedInViews.visibility = View.GONE
        binding.loginBtn.visibility = View.VISIBLE
    }
    binding.progressBar.visibility = View.INVISIBLE
}

fun logout() {
    binding.progressBar.visibility = View.VISIBLE
    authgear.logout(true, object : OnLogoutListener {
        override fun onLogout() {
            updateUi(authgear)
        }

        override fun onLogoutFailed(throwable: Throwable) {
            Log.d("TAG", throwable.toString())
        }
    })
}

fun openUserSettings() {
    authgear.open(Page.SETTINGS)
}

说明

请导入所有显示为无法解析的类。

检查点

在设备或模拟器上运行您的应用,并点击 Login。由于您已经在步骤 3中设置了重定向 URI,Authany 登录页面会随之打开,登录成功后会返回应用,显示用户的邮箱以及 User Settings 和 Logout 按钮。

演示应用截图

更多操作

获取登录状态

您可以使用用户的登录状态来判断用户是否已登录,并据此展示用户信息、登出按钮等内容,就像我们在步骤 4中做的那样。SessionState 反映的是 SDK 本地状态中记录的用户登录状态。这意味着即使 SessionState 为 AUTHENTICATED,如果会话已在远端被吊销,该会话仍可能是无效的。初始化 Authany SDK 后,请在合适的时机调用 fetchUserInfo 来更新 SessionState。

// After authgear.configure, it only reflect SDK local state.
// value can be NO_SESSION or AUTHENTICATED
val state = authgear.sessionState

SessionState 的值可以是 UNKNOWN、NO_SESSION 或 AUTHENTICATED。初始时,sessionState 为 UNKNOWN。调用 authgear.configure 后,如果找到了之前的会话,状态会变为 AUTHENTICATED;如果没有找到,则变为 NO_SESSION。

获取用户信息

在某些情况下,您可能需要通过 SDK 获取当前用户信息(例如像我们在步骤 4中那样,在界面上显示邮箱地址)。可以使用 fetchUserInfo 函数获取用户信息,参见示例。

在 HTTP 请求中使用访问令牌

每次使用访问令牌前都调用 refreshAccessTokenIfNeeded,该函数只有在访问令牌已过期时才会检查并发起网络请求。请将访问令牌放入应用请求的 Authorization 请求头中。如果您的项目中使用了 OKHttp,也可以使用 SDK 提供的拦截器扩展,详见说明。

说明

访问令牌是一种 JSON Web Token(JWT)。

try {
    authgear.refreshAccessTokenIfNeededSync()
} catch (e: OAuthException) {
    // Something went wrong
}

val accessToken = authgear.accessToken 
if (accessToken == null) {
    // The user is not logged in, or the token is expired.
    // It is up to the caller to decide how to handle this situation.
    // Typically, the request could be aborted
    // immediately as the response would be 401 anyways.
    return
}

val headers = mutableMapOf<String, String>()
headers["authorization"] = "Bearer $accessToken"

// Submit the request with the headers..

后续步骤

为了保护您的应用服务器免受未经授权的访问,您需要将 Authany 集成到后端。

后端/API 集成

Android SDK 参考文档

有关 Android SDK 的详细文档,请访问 Android SDK 参考文档