教程:从 Android 应用程序登录用户并调用 Microsoft Graph API
在本教程中,你要构建一个与 Microsoft Entra ID 集成的 Android 应用,用户可登录该应用并获取访问令牌以调用 Microsoft Graph API。
完成本教程后,该应用程序会接受任何公司或组织中使用 Microsoft Entra ID 的工作或学校帐户进行登录。
在本教程中:
- 在 Android Studio 中创建 Android 应用项目
- 在 Microsoft Entra 管理中心注册应用
- 添加代码以支持用户登录和注销
- 添加代码以调用 Microsoft Graph API
- 测试应用程序
先决条件
本教程工作原理
本教程中的应用将用户登录并代表他们获取数据。 此数据可通过受保护的 API (Microsoft Graph API) 进行访问,该 API 需要授权并且受 Microsoft 标识平台保护。
此示例使用适用于 Android 的 Microsoft 身份验证库(MSAL)以实现身份验证:com.microsoft.identity.client。
创建一个项目
如果还没有 Android 应用程序,请按照以下步骤新建项目。
- 打开 Android Studio,然后选择“启动新的 Android Studio 项目” 。
- 选择“基本活动”,再选择“下一步” 。
- 输入应用程序名称,例如 MSALAndroidapp。
- 记录要在后续步骤中使用的包名称。
- 将语言从“Kotlin” 更改为“Java” 。
- 将“最低 SDK API 级别”设置为“API 16”或更高级别,然后选择“完成”。
使用 Microsoft Entra ID 注册应用程序
提示
本文中的步骤可能因开始使用的门户而略有不同。
至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心。
如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。
浏览到“标识”>“应用程序”>“应用注册”。
选择“新注册”。
输入应用程序的名称。 应用的用户可能会看到此名称,你稍后可对其进行更改。
对于“支持的帐户类型”,请选择“任何组织目录(任何 Microsoft Entra 目录 - 多租户)中的帐户”。 要了解不同帐户类型的信息,请选择“帮我选择”选项。
选择“注册” 。
在“管理”下,选择“身份验证”>“添加平台”>“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": "00001111-aaaa-bbbb-3333-cccc4444", "authorization_user_agent": "WEBVIEW", "redirect_uri": "msauth://com.azuresamples.msalandroidapp/00001111%cccc4444%3D", "broker_redirect_uri_registered": true, "account_mode": "SINGLE", "authorities": [ { "type": "AAD", "audience": { "type": "AzureADMultipleOrgs", "tenant_id": "common" } } ] }
建议使用“WEBVIEW”。 如果要在应用中将“authorization_user_agent”配置为“BROWSER”,则需要进行以下更新。 a) 将 auth_config_single_account.json 更新为 "authorization_user_agent": "Browser"。 b) 更新 AndroidManifest.xml。 在应用中,转到 app>src>main>AndroidManifest.xml,将
BrowserTabActivity
活动添加为<application>
元素的子元素。 该条目允许 Microsoft Entra ID 在完成身份验证后回调应用程序:<!--Intent filter to capture System Browser or Authenticator calling back to our app after sign-in--> <activity android:name="com.microsoft.identity.client.BrowserTabActivity" android:exported="true"> <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=.
值。 它看起来应该类似于:com.azuresamples.msalandroidapp
。 - 使用“签名哈希”替换
android:path=
值。 确保签名哈希的开头有前导/
。 它看起来应该类似于:/aB1cD2eF3gH4+iJ5kL6-mN7oP8q=
。
还可以在应用注册的“身份验证”边栏选项卡中找到这些值。
- 使用包名称替换
将 MSAL 和相关库添加到项目
在 Android Studio 项目窗口中,导航到 app>build.gradle,并在“依赖项”部分添加以下库:
implementation 'com.microsoft.identity.client:msal:5.0.0' implementation 'com.android.volley:volley:1.2.1'
在 Android Studio 项目窗口中,打开 settings.gradle 并在“dependencyResolutionManagement>存储库”部分声明以下 maven 存储库 :
maven { url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' }
选择通知栏中的“立即同步”。
创建和更新所需片段
在 app>src>main>java>com.example(应用名称) 中。 创建以下 Android 片段:
- MSGraphRequestWrapper
- OnFragmentInteractionListener
- SingleAccountModeFragment
打开 MSGraphRequestWrapper.java 并将该代码替换为以下代码片段,从而使用 MSAL 提供的令牌调用 Microsoft Graph API:
package com.azuresamples.msalandroidapp; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.android.volley.DefaultRetryPolicy; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.toolbox.JsonObjectRequest; import com.android.volley.toolbox.Volley; import org.json.JSONObject; import java.util.HashMap; import java.util.Map; public class MSGraphRequestWrapper { private static final String TAG = MSGraphRequestWrapper.class.getSimpleName(); // See: https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints public static final String MS_GRAPH_ROOT_ENDPOINT = "https://microsoftgraph.chinacloudapi.cn/"; /** * Use Volley to make an HTTP request with * 1) a given MSGraph resource URL * 2) an access token * to obtain MSGraph data. **/ public static void callGraphAPIUsingVolley(@NonNull final Context context, @NonNull final String graphResourceUrl, @NonNull final String accessToken, @NonNull final Response.Listener<JSONObject> responseListener, @NonNull final Response.ErrorListener errorListener) { Log.d(TAG, "Starting volley request to graph"); /* Make sure we have a token to send to graph */ if (accessToken == null || accessToken.length() == 0) { return; } RequestQueue queue = Volley.newRequestQueue(context); JSONObject parameters = new JSONObject(); try { parameters.put("key", "value"); } catch (Exception e) { Log.d(TAG, "Failed to put parameters: " + e.toString()); } JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, graphResourceUrl, parameters, responseListener, errorListener) { @Override public Map<String, String> getHeaders() { Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "Bearer " + accessToken); return headers; } }; Log.d(TAG, "Adding HTTP GET to Queue, Request: " + request.toString()); request.setRetryPolicy(new DefaultRetryPolicy( 3000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); queue.add(request); } }
打开 OnFragmentInteractionListener.java,并将该代码替换为以下代码片段,从而允许不同片段之间进行通信:
package com.azuresamples.msalandroidapp; /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. * <p> * See the Android Training lesson <a href= * "http://developer.android.com/training/basics/fragments/communicating.html" * >Communicating with Other Fragments</a> for more information. */ public interface OnFragmentInteractionListener { }
打开 SingleAccountModeFragment.java 并将该代码替换为以下代码片段,从而初始化单帐户应用程序、加载用户帐户并获取用于调用 Microsoft Graph API 的令牌:
package com.azuresamples.msalandroidapp; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import com.android.volley.Response; import com.android.volley.VolleyError; import com.microsoft.identity.client.AuthenticationCallback; import com.microsoft.identity.client.IAccount; import com.microsoft.identity.client.IAuthenticationResult; import com.microsoft.identity.client.IPublicClientApplication; import com.microsoft.identity.client.ISingleAccountPublicClientApplication; import com.microsoft.identity.client.PublicClientApplication; import com.microsoft.identity.client.SilentAuthenticationCallback; import com.microsoft.identity.client.exception.MsalClientException; import com.microsoft.identity.client.exception.MsalException; import com.microsoft.identity.client.exception.MsalServiceException; import com.microsoft.identity.client.exception.MsalUiRequiredException; import org.json.JSONObject; /** * Implementation sample for 'Single account' mode. * <p> * If your app only supports one account being signed-in at a time, this is for you. * This requires "account_mode" to be set as "SINGLE" in the configuration file. * (Please see res/raw/auth_config_single_account.json for more info). * <p> * Please note that switching mode (between 'single' and 'multiple' might cause a loss of data. */ public class SingleAccountModeFragment extends Fragment { private static final String TAG = SingleAccountModeFragment.class.getSimpleName(); /* UI & Debugging Variables */ Button signInButton; Button signOutButton; Button callGraphApiInteractiveButton; Button callGraphApiSilentButton; TextView scopeTextView; TextView graphResourceTextView; TextView logTextView; TextView currentUserTextView; TextView deviceModeTextView; /* Azure AD Variables */ private ISingleAccountPublicClientApplication mSingleAccountApp; private IAccount mAccount; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment final View view = inflater.inflate(R.layout.fragment_single_account_mode, container, false); initializeUI(view); // Creates a PublicClientApplication object with res/raw/auth_config_single_account.json PublicClientApplication.createSingleAccountPublicClientApplication(getContext(), R.raw.auth_config_single_account, new IPublicClientApplication.ISingleAccountApplicationCreatedListener() { @Override public void onCreated(ISingleAccountPublicClientApplication application) { /** * This test app assumes that the app is only going to support one account. * This requires "account_mode" : "SINGLE" in the config json file. **/ mSingleAccountApp = application; loadAccount(); } @Override public void onError(MsalException exception) { displayError(exception); } }); return view; } /** * Initializes UI variables and callbacks. */ private void initializeUI(@NonNull final View view) { signInButton = view.findViewById(R.id.btn_signIn); signOutButton = view.findViewById(R.id.btn_removeAccount); callGraphApiInteractiveButton = view.findViewById(R.id.btn_callGraphInteractively); callGraphApiSilentButton = view.findViewById(R.id.btn_callGraphSilently); scopeTextView = view.findViewById(R.id.scope); graphResourceTextView = view.findViewById(R.id.msgraph_url); logTextView = view.findViewById(R.id.txt_log); currentUserTextView = view.findViewById(R.id.current_user); deviceModeTextView = view.findViewById(R.id.device_mode); final String defaultGraphResourceUrl = MSGraphRequestWrapper.MS_GRAPH_ROOT_ENDPOINT + "v1.0/me"; graphResourceTextView.setText(defaultGraphResourceUrl); signInButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (mSingleAccountApp == null) { return; } mSingleAccountApp.signIn(getActivity(), null, getScopes(), getAuthInteractiveCallback()); } }); signOutButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (mSingleAccountApp == null) { return; } /** * Removes the signed-in account and cached tokens from this app (or device, if the device is in shared mode). */ mSingleAccountApp.signOut(new ISingleAccountPublicClientApplication.SignOutCallback() { @Override public void onSignOut() { mAccount = null; updateUI(); showToastOnSignOut(); } @Override public void onError(@NonNull MsalException exception) { displayError(exception); } }); } }); callGraphApiInteractiveButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (mSingleAccountApp == null) { return; } /** * If acquireTokenSilent() returns an error that requires an interaction (MsalUiRequiredException), * invoke acquireToken() to have the user resolve the interrupt interactively. * * Some example scenarios are * - password change * - the resource you're acquiring a token for has a stricter set of requirement than your Single Sign-On refresh token. * - you're introducing a new scope which the user has never consented for. */ mSingleAccountApp.acquireToken(getActivity(), getScopes(), getAuthInteractiveCallback()); } }); callGraphApiSilentButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mSingleAccountApp == null) { return; } /** * Once you've signed the user in, * you can perform acquireTokenSilent to obtain resources without interrupting the user. */ mSingleAccountApp.acquireTokenSilentAsync(getScopes(), mAccount.getAuthority(), getAuthSilentCallback()); } }); } @Override public void onResume() { super.onResume(); /** * The account may have been removed from the device (if broker is in use). * * In shared device mode, the account might be signed in/out by other apps while this app is not in focus. * Therefore, we want to update the account state by invoking loadAccount() here. */ loadAccount(); } /** * Extracts a scope array from a text field, * i.e. from "User.Read User.ReadWrite" to ["user.read", "user.readwrite"] */ private String[] getScopes() { return scopeTextView.getText().toString().toLowerCase().split(" "); } /** * Load the currently signed-in account, if there's any. */ 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. mAccount = activeAccount; updateUI(); } @Override public void onAccountChanged(@Nullable IAccount priorAccount, @Nullable IAccount currentAccount) { if (currentAccount == null) { // Perform a cleanup task as the signed-in account changed. showToastOnSignOut(); } } @Override public void onError(@NonNull MsalException exception) { displayError(exception); } }); } /** * Callback used in for silent acquireToken calls. */ private SilentAuthenticationCallback getAuthSilentCallback() { return new SilentAuthenticationCallback() { @Override public void onSuccess(IAuthenticationResult authenticationResult) { Log.d(TAG, "Successfully authenticated"); /* Successfully got a token, use it to call a protected resource - MSGraph */ callGraphAPI(authenticationResult); } @Override public void onError(MsalException exception) { /* Failed to acquireToken */ Log.d(TAG, "Authentication failed: " + exception.toString()); displayError(exception); if (exception instanceof MsalClientException) { /* Exception inside MSAL, more info inside MsalError.java */ } else if (exception instanceof MsalServiceException) { /* Exception when communicating with the STS, likely config issue */ } else if (exception instanceof MsalUiRequiredException) { /* Tokens expired or no session, retry with interactive */ } } }; } /** * Callback used for interactive request. * If succeeds we use the access token to call the Microsoft Graph. * Does not check cache. */ 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"); Log.d(TAG, "ID Token: " + authenticationResult.getAccount().getClaims().get("id_token")); /* Update account */ mAccount = authenticationResult.getAccount(); updateUI(); /* call graph */ callGraphAPI(authenticationResult); } @Override public void onError(MsalException exception) { /* Failed to acquireToken */ Log.d(TAG, "Authentication failed: " + exception.toString()); displayError(exception); if (exception instanceof MsalClientException) { /* Exception inside MSAL, more info inside MsalError.java */ } else if (exception instanceof MsalServiceException) { /* Exception when communicating with the STS, likely config issue */ } } @Override public void onCancel() { /* User canceled the authentication */ Log.d(TAG, "User cancelled login."); } }; } /** * Make an HTTP request to obtain MSGraph data */ private void callGraphAPI(final IAuthenticationResult authenticationResult) { MSGraphRequestWrapper.callGraphAPIUsingVolley( getContext(), graphResourceTextView.getText().toString(), authenticationResult.getAccessToken(), new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { /* Successfully called graph, process data and send to UI */ Log.d(TAG, "Response: " + response.toString()); displayGraphResult(response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.d(TAG, "Error: " + error.toString()); displayError(error); } }); } // // Helper methods manage UI updates // ================================ // displayGraphResult() - Display the graph response // displayError() - Display the graph response // updateSignedInUI() - Updates UI when the user is signed in // updateSignedOutUI() - Updates UI when app sign out succeeds // /** * Display the graph response */ private void displayGraphResult(@NonNull final JSONObject graphResponse) { logTextView.setText(graphResponse.toString()); } /** * Display the error message */ private void displayError(@NonNull final Exception exception) { logTextView.setText(exception.toString()); } /** * Updates UI based on the current account. */ private void updateUI() { if (mAccount != null) { signInButton.setEnabled(false); signOutButton.setEnabled(true); callGraphApiInteractiveButton.setEnabled(true); callGraphApiSilentButton.setEnabled(true); currentUserTextView.setText(mAccount.getUsername()); } else { signInButton.setEnabled(true); signOutButton.setEnabled(false); callGraphApiInteractiveButton.setEnabled(false); callGraphApiSilentButton.setEnabled(false); currentUserTextView.setText("None"); } deviceModeTextView.setText(mSingleAccountApp.isSharedDevice() ? "Shared" : "Non-shared"); } /** * Updates UI when app sign out succeeds */ private void showToastOnSignOut() { final String signOutText = "Signed Out."; currentUserTextView.setText(""); Toast.makeText(getContext(), signOutText, Toast.LENGTH_SHORT) .show(); } }
打开 MainActivity.java 并将该代码替换为以下代码片段以管理 UI。
package com.azuresamples.msalandroidapp; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.view.GravityCompat; import android.view.MenuItem; import android.view.View; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import com.google.android.material.navigation.NavigationView; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, OnFragmentInteractionListener{ enum AppFragment { SingleAccount } private AppFragment mCurrentFragment; private ConstraintLayout mContentMain; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mContentMain = findViewById(R.id.content_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); DrawerLayout drawer = findViewById(R.id.drawer_layout); NavigationView navigationView = findViewById(R.id.nav_view); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawer.addDrawerListener(toggle); toggle.syncState(); navigationView.setNavigationItemSelectedListener(this); //Set default fragment navigationView.setCheckedItem(R.id.nav_single_account); setCurrentFragment(AppFragment.SingleAccount); } @Override public boolean onNavigationItemSelected(final MenuItem item) { final DrawerLayout drawer = findViewById(R.id.drawer_layout); drawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { } @Override public void onDrawerClosed(@NonNull View drawerView) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_single_account) { setCurrentFragment(AppFragment.SingleAccount); } drawer.removeDrawerListener(this); } @Override public void onDrawerStateChanged(int newState) { } }); drawer.closeDrawer(GravityCompat.START); return true; } private void setCurrentFragment(final AppFragment newFragment){ if (newFragment == mCurrentFragment) { return; } mCurrentFragment = newFragment; setHeaderString(mCurrentFragment); displayFragment(mCurrentFragment); } private void setHeaderString(final AppFragment fragment){ switch (fragment) { case SingleAccount: getSupportActionBar().setTitle("Single Account Mode"); return; } } private void displayFragment(final AppFragment fragment){ switch (fragment) { case SingleAccount: attachFragment(new com.azuresamples.msalandroidapp.SingleAccountModeFragment()); return; } } private void attachFragment(final Fragment fragment) { getSupportFragmentManager() .beginTransaction() .setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .replace(mContentMain.getId(),fragment) .commit(); } }
注意
确保更新包名称以匹配 Android 项目包名称。
Layout
布局是一个文件,用于定义用户界面的视觉结构和外观,指定 UI 组件的排列方式。 它是用 XML 编写的。 如果想根据本教程对 UI 进行建模,请参阅下面提供的 XML 示例:
在 app>src>main>res>layout>activity_main.xml 中。 将 activity_main.xml 的内容替换为以下代码片段,从而显示按钮和文本框:
<?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:openDrawer="start"> <include layout="@layout/app_bar_main" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" app:headerLayout="@layout/nav_header_main" app:menu="@menu/activity_main_drawer" /> </androidx.drawerlayout.widget.DrawerLayout>
在 app>src>main>res>layout>app_bar_main.xml 中。 如果文件夹中没有 app_bar_main.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </com.google.android.material.appbar.AppBarLayout> <include layout="@layout/content_main" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
在 app>src>main>res>layout>content_main.xml 中。 如果文件夹中没有 content_main.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/content_main" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".MainActivity" tools:showIn="@layout/app_bar_main"> </androidx.constraintlayout.widget.ConstraintLayout>
在 app>src>main>res>layout>fragment_m_s_graph_request_wrapper.xml 中。 如果文件夹中没有 fragment_m_s_graph_request_wrapper.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MSGraphRequestWrapper"> <!-- TODO: Update blank fragment layout --> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/hello_blank_fragment" /> </FrameLayout>
在 app>src>main>res>layout>fragment_on_interaction_listener.xml 中。 如果文件夹中没有 fragment_on_interaction_listener.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".OnFragmentInteractionListener"> <!-- TODO: Update blank fragment layout --> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/hello_blank_fragment" /> </FrameLayout>
在 app>src>main>res>layout>fragment_single_account_mode.xml 中。 如果文件夹中没有 fragment_single_account_mode.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".SingleAccountModeFragment"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".SingleAccountModeFragment"> <LinearLayout android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingTop="5dp" android:paddingBottom="5dp" android:weightSum="10"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:layout_gravity="center_vertical" android:textStyle="bold" android:text="Scope" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:layout_weight="7"> <EditText android:id="@+id/scope" android:layout_height="wrap_content" android:layout_width="match_parent" android:text="user.read" android:textSize="12sp" /> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:paddingLeft="5dp" android:text="Type in scopes delimited by space" android:textSize="10sp" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingTop="5dp" android:paddingBottom="5dp" android:weightSum="10"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:layout_gravity="center_vertical" android:textStyle="bold" android:text="MSGraph Resource URL" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:layout_weight="7"> <EditText android:id="@+id/msgraph_url" android:layout_height="wrap_content" android:layout_width="match_parent" android:textSize="12sp" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingTop="5dp" android:paddingBottom="5dp" android:weightSum="10"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:textStyle="bold" android:text="Signed-in user" /> <TextView android:id="@+id/current_user" android:layout_width="0dp" android:layout_height="wrap_content" android:paddingLeft="5dp" android:layout_weight="7" android:text="None" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingTop="5dp" android:paddingBottom="5dp" android:weightSum="10"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:textStyle="bold" android:text="Device mode" /> <TextView android:id="@+id/device_mode" android:layout_width="0dp" android:layout_height="wrap_content" android:paddingLeft="5dp" android:layout_weight="7" android:text="None" /> </LinearLayout> <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/btn_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/btn_removeAccount" 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/btn_callGraphInteractively" 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/btn_callGraphSilently" 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: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> </LinearLayout> </FrameLayout>
在 app>src>main>res>layout>nav_header_main.xml 中。 如果文件夹中没有 nav_header_main.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="@dimen/nav_header_height" android:background="@drawable/side_nav_bar" android:gravity="bottom" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin" android:theme="@style/ThemeOverlay.AppCompat.Dark"> <ImageView android:id="@+id/imageView" android:layout_width="66dp" android:layout_height="72dp" android:contentDescription="@string/nav_header_desc" android:paddingTop="@dimen/nav_header_vertical_spacing" app:srcCompat="@drawable/microsoft_logo" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/nav_header_vertical_spacing" android:text="Azure Samples" android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="MSAL Android" /> </LinearLayout>
在 app>src>main>res>menu>activity_main_drawer.xml 中。 如果文件夹中没有 activity_main_drawer.xml,请创建并添加以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:showIn="navigation_view"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_single_account" android:icon="@drawable/ic_single_account_24dp" android:title="Single Account Mode" /> </group> </menu>
在 app>src>main>res>values>dimens.xml 中。 将 dimens.xml 的内容替换为以下代码片段:
<resources> <dimen name="fab_margin">16dp</dimen> <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> <dimen name="nav_header_height">176dp</dimen> <dimen name="nav_header_vertical_spacing">8dp</dimen> </resources>
在 app>src>main>res>values>colors.xml 中。 将 colors.xml 的内容替换为以下代码片段:
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="purple_200">#FFBB86FC</color> <color name="purple_500">#FF6200EE</color> <color name="purple_700">#FF3700B3</color> <color name="teal_200">#FF03DAC5</color> <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> <color name="colorPrimary">#008577</color> <color name="colorPrimaryDark">#00574B</color> <color name="colorAccent">#D81B60</color> </resources>
在 app>src>main>res>values>strings.xml 中。 将 strings.xml 的内容替换为以下代码片段:
<resources> <string name="app_name">MSALAndroidapp</string> <string name="action_settings">Settings</string> <!-- Strings used for fragments for navigation --> <string name="first_fragment_label">First Fragment</string> <string name="second_fragment_label">Second Fragment</string> <string name="nav_header_desc">Navigation header</string> <string name="navigation_drawer_open">Open navigation drawer</string> <string name="navigation_drawer_close">Close navigation drawer</string> <string name="next">Next</string> <string name="previous">Previous</string> <string name="hello_first_fragment">Hello first fragment</string> <string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string> <!-- TODO: Remove or change this placeholder text --> <string name="hello_blank_fragment">Hello blank fragment</string> </resources>
在 app>src>main>res>values>styles.xml 中。 如果文件夹中没有 styles.xml,请创建并添加以下代码片段:
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> <style name="AppTheme.NoActionBar"> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> </style> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> </resources>
在 app>src>main>res>values>themes.xml 中。 将 themes.xml 的内容替换为以下代码片段:
<resources xmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> <style name="Theme.MSALAndroidapp" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <!-- Primary brand color. --> <item name="colorPrimary">@color/purple_500</item> <item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorOnPrimary">@color/white</item> <!-- Secondary brand color. --> <item name="colorSecondary">@color/teal_200</item> <item name="colorSecondaryVariant">@color/teal_700</item> <item name="colorOnSecondary">@color/black</item> <!-- Status bar color. --> <item name="android:statusBarColor" tools:targetApi="21">?attr/colorPrimaryVariant</item> <!-- Customize your theme here. --> </style> <style name="Theme.MSALAndroidapp.NoActionBar"> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> </style> <style name="Theme.MSALAndroidapp.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <style name="Theme.MSALAndroidapp.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> </resources>
在 app>src>main>res>drawable>ic_single_account_24dp.xml 中。 如果文件夹中没有 ic_single_account_24dp.xml,请创建并添加以下代码片段:
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> </vector>
在 app>src>main>res>drawable>side_nav_bar.xml 中。 如果文件夹中没有 side_nav_bar.xml,请创建并添加以下代码片段:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:angle="135" android:centerColor="#009688" android:endColor="#00695C" android:startColor="#4DB6AC" android:type="linear" /> </shape>
在 app>src>main>res>drawable 中。 在文件夹中,添加名为
microsoft_logo.png
的 png Microsoft 徽标。
在 XML 中声明 UI 可将应用的呈现与控制其行为的代码分开。 若要了解有关 Android 布局的详细信息,请参阅布局
测试应用程序
在本地运行
构建应用并将其部署到测试设备或模拟器。 现在应该可以登录并获取 Microsoft Entra ID 的令牌。
你登录后,此应用将显示从 Microsoft Graph /me
终结点返回的数据。
同意
任何用户首次登录你的应用时,Microsoft 标识都会提示他们同意请求的权限。 某些 Microsoft Entra 租户已禁用用户同意功能,这要求管理员代表所有用户同意。 若要支持此场景,需创建自己的租户或获得管理员的同意。
清理资源
如果不再需要,请删除注册应用程序 步骤中创建的应用对象。
帮助和支持
如果需要帮助、需要报告问题,或者需要详细了解支持选项,请参阅面向开发人员的帮助和支持。
后续步骤
若要探索更复杂的场景,请参阅 GitHub 上已完成的工作代码示例。
有关在多部分场景系列中构建调用受保护 Web API 的移动应用的详细信息,请参阅: