你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
教程:从 Android 应用程序登录用户并调用 Microsoft Graph API
在本教程中,你要构建一个与 Microsoft 标识平台集成的 Android 应用,用户可登录该应用并获取访问令牌以调用 Microsoft Graph API。
完成本教程后,应用程序将接受个人 Microsoft 帐户(包括 outlook.com、live.com 和其他帐户)进行登录,还能够接受使用 Azure Active Directory 的任何公司或组织的工作或学校帐户进行登录。
在本教程中:
- 在 Android Studio 中创建 Android 应用项目
- 在 Azure 门户中注册应用
- 添加代码以支持用户登录和注销
- 添加代码以调用 Microsoft Graph API
- 测试应用程序
先决条件
- Android Studio 3.5+
本教程工作原理
本教程中的应用会将用户登录并代表他们获取数据。 该数据可通过一个受保护的 API (Microsoft 图形 API) 进行访问,该 API 需要授权并且受 Microsoft 标识平台保护。
更具体说来:
- 你的应用将通过浏览器或 Microsoft Authenticator 和 Intune 公司门户登录用户。
- 最终用户将接受应用程序请求的权限。
- 将为你的应用颁发 Microsoft Graph API 的一个访问令牌。
- 该访问令牌将包括在对 Web API 的 HTTP 请求中。
- 处理 Microsoft Graph 响应。
该示例使用适用于 Android 的 Microsoft 身份验证库 (MSAL) 来实现身份验证:com.microsoft.identity.client。
MSAL 将自动续订令牌,在设备上的其他应用之间提供单一登录 (SSO),并管理帐户。
本教程演示简化的示例,介绍如何使用适用于 Android 的 MSAL。 为简单起见,本教程仅使用“单帐户模式”。 若要探索更复杂的场景,请参阅 GitHub 上已完成的工作代码示例。
创建一个项目
如果你还没有 Android 应用程序,请按照以下步骤设置新项目。
- 打开 Android Studio,然后选择“启动新的 Android Studio 项目” 。
- 选择“基本活动”,再选择“下一步” 。
- 命名应用程序。
- 保存包名称。 以后需将它输入 Azure 门户中。
- 将语言从“Kotlin” 更改为“Java” 。
- 将“最低 API 级别” 设置为 API 19 或更高,然后单击“完成”。
- 在项目视图的下拉列表中选择“项目” ,以便显示源和非源的项目文件,然后打开 app/build.gradle,将
targetSdkVersion设置为28。
与 Microsoft 身份验证库集成
注册应用程序
登录 Azure 门户。
如果有权访问多个租户,请使用顶部菜单中的“目录 + 订阅”筛选器
,以切换到要在其中注册应用程序的租户。搜索并选择“Azure Active Directory” 。
在“管理”下,选择“应用注册”>“新建注册” 。
输入应用程序的名称。 应用的用户可能会看到此名称,你稍后可对其进行更改。
选择“注册” 。
在“管理”下,选择“身份验证”>“添加平台”>“Android” 。
输入项目的包名称。 如果下载了代码,则该值为
com.azuresamples.msalandroidapp。在“配置 Android 应用”页的“签名哈希”部分,单击“生成开发签名哈希”,并复制 KeyTool 命令,以在平台中使用。
安装 KeyTool.exe,使其作为 Java 开发工具包 (JDK) 的一部分。 还必须安装 OpenSSL 工具才能执行 KeyTool 命令。 有关详细信息,请参阅有关如何生成密钥的 Android 文档。
生成由 KeyTool 生成的签名哈希。
选择“配置”并保存出现在“Android 配置”页中的“MSAL 配置”,以便在稍后配置应用时输入它 。
选择“完成”。
配置应用程序
在 Android Studio 的项目窗格中,导航到 app\src\main\res。
右键单击“res” ,选择“新建” > “目录”。 输入
raw作为新目录名称,然后单击“确定”。在 app>src>main>res>raw 中,新建名为
auth_config_single_account.json的 JSON 文件,然后粘贴以前保存的 MSAL 配置。在“重定向 URI”下方,粘贴:
"account_mode" : "SINGLE",配置文件应与如下示例类似:
{ "client_id" : "0984a7b6-bc13-4141-8b0d-8f767e136bb7", "authorization_user_agent" : "DEFAULT", "redirect_uri" : "msauth://com.azuresamples.msalandroidapp/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D", "broker_redirect_uri_registered" : true, "account_mode" : "SINGLE", "authorities" : [ { "type": "AAD", "audience": { "type": "AzureADandPersonalMicrosoftAccount", "tenant_id": "common" } } ] }本教程仅演示如何在单帐户模式下配置应用。 查看文档,详细了解单帐户模式与多帐户模式以及配置应用
在 app>src>main>AndroidManifest.xml 中,将以下
BrowserTabActivity活动添加到应用程序主体。 该条目允许 Microsoft 在完成身份验证后回调应用程序:<!--Intent filter to capture System Browser or Authenticator calling back to our app after sign-in--> <activity android:name="com.microsoft.identity.client.BrowserTabActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="msauth" android:host="Enter_the_Package_Name" android:path="/Enter_the_Signature_Hash" /> </intent-filter> </activity>将
android:host=值替换为在 Azure 门户中注册的包名称。 将android:path=值替换为在 Azure 门户中注册的密钥哈希。 签名哈希不应进行 URL 编码。 确保签名哈希的开头有前导/。将用来替换
android:host值的“包名称”应类似于com.azuresamples.msalandroidapp。 将用来替换android:path值的“签名哈希”应类似于/1wIqXSqBj7w+h11ZifsnqwgyKrY=。还可以在应用注册的“身份验证”边栏选项卡中找到这些值。 请注意,重定向 URI 看起来类似于
msauth://com.azuresamples.msalandroidapp/1wIqXSqBj7w%2Bh11ZifsnqwgyKrY%3D。 尽管签名哈希会在此值末尾进行 URL 编码,但签名哈希不应在android:path值中进行 URL 编码。
使用 MSAL
将 MSAL 添加到项目
在 Android Studio 项目窗口中,导航到 app>build.gradle,然后添加以下内容 :
apply plugin: 'com.android.application' allprojects { repositories { mavenCentral() google() mavenLocal() maven { url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' } maven { name "vsts-maven-adal-android" url "https://identitydivision.pkgs.visualstudio.com/_packaging/AndroidADAL/maven/v1" credentials { username System.getenv("ENV_VSTS_MVN_ANDROIDADAL_USERNAME") != null ? System.getenv("ENV_VSTS_MVN_ANDROIDADAL_USERNAME") : project.findProperty("vstsUsername") password System.getenv("ENV_VSTS_MVN_ANDROIDADAL_ACCESSTOKEN") != null ? System.getenv("ENV_VSTS_MVN_ANDROIDADAL_ACCESSTOKEN") : project.findProperty("vstsMavenAccessToken") } } jcenter() } } dependencies{ implementation 'com.microsoft.identity.client:msal:2.+' implementation 'com.microsoft.graph:microsoft-graph:1.5.+' } packagingOptions{ exclude("META-INF/jersey-module-version") }
要求的导入
将以下内容添加到 app>src>main>java>com.example(yourapp)>MainActivity.java 的顶部
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.google.gson.JsonObject;
import com.microsoft.graph.authentication.IAuthenticationProvider; //Imports the Graph sdk Auth interface
import com.microsoft.graph.concurrency.ICallback;
import com.microsoft.graph.core.ClientException;
import com.microsoft.graph.http.IHttpRequest;
import com.microsoft.graph.models.extensions.*;
import com.microsoft.graph.requests.extensions.GraphServiceClient;
import com.microsoft.identity.client.AuthenticationCallback; // Imports MSAL auth methods
import com.microsoft.identity.client.*;
import com.microsoft.identity.client.exception.*;
实例化 PublicClientApplication
初始化变量
private final static String[] SCOPES = {"Files.Read"};
/* Azure AD v2 Configs */
final static String AUTHORITY = "https://login.microsoftonline.com/common";
private ISingleAccountPublicClientApplication mSingleAccountApp;
private static final String TAG = MainActivity.class.getSimpleName();
/* UI & Debugging Variables */
Button signInButton;
Button signOutButton;
Button callGraphApiInteractiveButton;
Button callGraphApiSilentButton;
TextView logTextView;
TextView currentUserTextView;
onCreate
在 MainActivity 类中,参阅下方的 onCreate() 方法以使用 SingleAccountPublicClientApplication 实例化 MSAL。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initializeUI();
PublicClientApplication.createSingleAccountPublicClientApplication(getApplicationContext(),
R.raw.auth_config_single_account, new IPublicClientApplication.ISingleAccountApplicationCreatedListener() {
@Override
public void onCreated(ISingleAccountPublicClientApplication application) {
mSingleAccountApp = application;
loadAccount();
}
@Override
public void onError(MsalException exception) {
displayError(exception);
}
});
}
loadAccount
//When app comes to the foreground, load existing account to determine if user is signed in
private void loadAccount() {
if (mSingleAccountApp == null) {
return;
}
mSingleAccountApp.getCurrentAccountAsync(new ISingleAccountPublicClientApplication.CurrentAccountCallback() {
@Override
public void onAccountLoaded(@Nullable IAccount activeAccount) {
// You can use the account data to update your UI or your app database.
updateUI(activeAccount);
}
@Override
public void onAccountChanged(@Nullable IAccount priorAccount, @Nullable IAccount currentAccount) {
if (currentAccount == null) {
// Perform a cleanup task as the signed-in account changed.
performOperationOnSignOut();
}
}
@Override
public void onError(@NonNull MsalException exception) {
displayError(exception);
}
});
}
initializeUI
侦听按钮并相应地调用方法或日志错误。
private void initializeUI(){
signInButton = findViewById(R.id.signIn);
callGraphApiSilentButton = findViewById(R.id.callGraphSilent);
callGraphApiInteractiveButton = findViewById(R.id.callGraphInteractive);
signOutButton = findViewById(R.id.clearCache);
logTextView = findViewById(R.id.txt_log);
currentUserTextView = findViewById(R.id.current_user);
//Sign in user
signInButton.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
if (mSingleAccountApp == null) {
return;
}
mSingleAccountApp.signIn(MainActivity.this, null, SCOPES, getAuthInteractiveCallback());
}
});
//Sign out user
signOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mSingleAccountApp == null){
return;
}
mSingleAccountApp.signOut(new ISingleAccountPublicClientApplication.SignOutCallback() {
@Override
public void onSignOut() {
updateUI(null);
performOperationOnSignOut();
}
@Override
public void onError(@NonNull MsalException exception){
displayError(exception);
}
});
}
});
//Interactive
callGraphApiInteractiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mSingleAccountApp == null) {
return;
}
mSingleAccountApp.acquireToken(MainActivity.this, SCOPES, getAuthInteractiveCallback());
}
});
//Silent
callGraphApiSilentButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mSingleAccountApp == null){
return;
}
mSingleAccountApp.acquireTokenSilentAsync(SCOPES, AUTHORITY, getAuthSilentCallback());
}
});
}
重要
使用 MSAL 注销会从应用程序中删除有关用户的所有已知信息,但是用户的设备上仍然有一个活动会话。 如果用户尝试再次登录,则可能会看到登录 UI,但由于设备会话仍处于活动状态,可能无需重新输入其凭据。
getAuthInteractiveCallback
用于交互式请求的回调。
private AuthenticationCallback getAuthInteractiveCallback() {
return new AuthenticationCallback() {
@Override
public void onSuccess(IAuthenticationResult authenticationResult) {
/* Successfully got a token, use it to call a protected resource - MSGraph */
Log.d(TAG, "Successfully authenticated");
/* Update UI */
updateUI(authenticationResult.getAccount());
/* call graph */
callGraphAPI(authenticationResult);
}
@Override
public void onError(MsalException exception) {
/* Failed to acquireToken */
Log.d(TAG, "Authentication failed: " + exception.toString());
displayError(exception);
}
@Override
public void onCancel() {
/* User canceled the authentication */
Log.d(TAG, "User cancelled login.");
}
};
}
getAuthSilentCallback
用于无提示请求的回调
private SilentAuthenticationCallback getAuthSilentCallback() {
return new SilentAuthenticationCallback() {
@Override
public void onSuccess(IAuthenticationResult authenticationResult) {
Log.d(TAG, "Successfully authenticated");
callGraphAPI(authenticationResult);
}
@Override
public void onError(MsalException exception) {
Log.d(TAG, "Authentication failed: " + exception.toString());
displayError(exception);
}
};
}
调用 Microsoft Graph API
以下代码演示如何使用 Graph SDK 调用 GraphAPI。
callGraphAPI
private void callGraphAPI(IAuthenticationResult authenticationResult) {
final String accessToken = authenticationResult.getAccessToken();
IGraphServiceClient graphClient =
GraphServiceClient
.builder()
.authenticationProvider(new IAuthenticationProvider() {
@Override
public void authenticateRequest(IHttpRequest request) {
Log.d(TAG, "Authenticating request," + request.getRequestUrl());
request.addHeader("Authorization", "Bearer " + accessToken);
}
})
.buildClient();
graphClient
.me()
.drive()
.buildRequest()
.get(new ICallback<Drive>() {
@Override
public void success(final Drive drive) {
Log.d(TAG, "Found Drive " + drive.id);
displayGraphResult(drive.getRawObject());
}
@Override
public void failure(ClientException ex) {
displayError(ex);
}
});
}
添加 UI
活动
如果要根据本教程为 UI 建模,则以下方法可提供有关更新文本和侦听按钮的指导。
updateUI
根据登录状态启用/禁用按钮,并设置文本。
private void updateUI(@Nullable final IAccount account) {
if (account != null) {
signInButton.setEnabled(false);
signOutButton.setEnabled(true);
callGraphApiInteractiveButton.setEnabled(true);
callGraphApiSilentButton.setEnabled(true);
currentUserTextView.setText(account.getUsername());
} else {
signInButton.setEnabled(true);
signOutButton.setEnabled(false);
callGraphApiInteractiveButton.setEnabled(false);
callGraphApiSilentButton.setEnabled(false);
currentUserTextView.setText("");
logTextView.setText("");
}
}
displayError
private void displayError(@NonNull final Exception exception) {
logTextView.setText(exception.toString());
}
displayGraphResult
private void displayGraphResult(@NonNull final JsonObject graphResponse) {
logTextView.setText(graphResponse.toString());
}
performOperationOnSignOut
在 UI 中更新文本以表示注销的方法。
private void performOperationOnSignOut() {
final String signOutText = "Signed Out.";
currentUserTextView.setText("");
Toast.makeText(getApplicationContext(), signOutText, Toast.LENGTH_SHORT)
.show();
}
布局
示例 activity_main.xml 文件,显示按钮和文本框。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:weightSum="10">
<Button
android:id="@+id/signIn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:gravity="center"
android:text="Sign In"/>
<Button
android:id="@+id/clearCache"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:gravity="center"
android:text="Sign Out"
android:enabled="false"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/callGraphInteractive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:text="Get Graph Data Interactively"
android:enabled="false"/>
<Button
android:id="@+id/callGraphSilent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:text="Get Graph Data Silently"
android:enabled="false"/>
</LinearLayout>
<TextView
android:text="Getting Graph Data..."
android:textColor="#3f3f3f"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:id="@+id/graphData"
android:visibility="invisible"/>
<TextView
android:id="@+id/current_user"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:layout_weight="0.8"
android:text="Account info goes here..." />
<TextView
android:id="@+id/txt_log"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:layout_weight="0.8"
android:text="Output goes here..." />
</LinearLayout>
测试应用
在本地运行
构建应用并将其部署到测试设备或模拟器。 你应能够登录并获取 Azure AD 或个人 Microsoft 帐户的令牌。
你登录后,此应用将显示从 Microsoft Graph /me 终结点返回的数据。
PR 4
同意
任何用户首次登录你的应用时,Microsoft 标识都将提示他们同意所请求的权限。 某些 Azure AD 租户已禁用用户同意功能,这要求管理员代表所有用户同意。 若要支持此场景,需创建自己的租户或获得管理员的同意。
清理资源
如果不再需要,请删除注册应用程序 步骤中创建的应用对象。
帮助和支持
如果需要帮助、需要报告问题,或者需要详细了解支持选项,请参阅面向开发人员的帮助和支持。
后续步骤
在我们的多部分场景系列中,详细了解如何构建可调用受保护 Web API 的移动应用。