教程:从 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 应用程序,请按照以下步骤新建项目。

  1. 打开 Android Studio,然后选择“启动新的 Android Studio 项目” 。
  2. 选择“基本活动”,再选择“下一步” 。
  3. 输入应用程序名称,例如 MSALAndroidapp
  4. 记录要在后续步骤中使用的包名称。
  5. 将语言从“Kotlin” 更改为“Java” 。
  6. 将“最低 SDK API 级别”设置为“API 16”或更高级别,然后选择“完成”。

使用 Microsoft Entra ID 注册应用程序

提示

本文中的步骤可能因开始使用的门户而略有不同。

  1. 至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心

  2. 如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。

  3. 浏览到“标识”>“应用程序”>“应用注册”。

  4. 选择“新注册”。

  5. 输入应用程序的名称。 应用的用户可能会看到此名称,你稍后可对其进行更改。

  6. 对于“支持的帐户类型”,请选择“任何组织目录(任何 Microsoft Entra 目录 - 多租户)中的帐户”。 要了解不同帐户类型的信息,请选择“帮我选择”选项。

  7. 选择“注册” 。

  8. 在“管理”下,选择“身份验证”>“添加平台”>“Android” 。

  9. 输入项目的包名称。 如果下载了示例代码,则该值为 com.azuresamples.msalandroidapp

  10. 在“配置 Android 应用”窗格的“签名哈希”部分,点击“生成开发签名哈希”,并将 KeyTool 命令复制到命令行。

    • 安装 KeyTool.exe,使其作为 Java 开发工具包 (JDK) 的一部分。 还必须安装 OpenSSL 工具才能执行 KeyTool 命令。 有关详细信息,请参阅 有关生成密钥的 Android 文档
  11. 生成由 KeyTool 生成的签名哈希

  12. 选择“配置”并保存在“Android 配置”窗格中出现的“MSAL 配置”,以便稍后配置应用时输入该内容。

  13. 选择“完成”。

配置应用程序

  1. 在 Android Studio 的项目窗格中,导航到 app\src\main\res

  2. 右键单击“res” ,选择“新建” > “目录”。 输入 raw 作为新目录名称,然后选择“确定”。

  3. 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"
          }
        }
      ]
    }
    

    由于本教程仅演示如何在单帐户模式下配置应用,请参阅 单帐户与多帐户模式配置应用 以获取详细信息

  4. 建议使用“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 和相关库添加到项目

  1. 在 Android Studio 项目窗口中,导航到 app>build.gradle,并在“依赖项”部分添加以下库:

     implementation 'com.microsoft.identity.client:msal:5.0.0'
     implementation 'com.android.volley:volley:1.2.1'
    
  2. 在 Android Studio 项目窗口中,打开 settings.gradle 并在“dependencyResolutionManagement>存储库”部分声明以下 maven 存储库 :

     maven {
          url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1'
     }
    
  3. 选择通知栏中的“立即同步”。

创建和更新所需片段

  1. app>src>main>java>com.example(应用名称) 中。 创建以下 Android 片段:

    • MSGraphRequestWrapper
    • OnFragmentInteractionListener
    • SingleAccountModeFragment
  2. 打开 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);
         }
     }
    
  3. 打开 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 {
     }
    
  4. 打开 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();
         }
     }
    
  5. 打开 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 示例:

  1. 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>
    
  2. 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>
    
  3. 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>
    
  4. 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>
    
  5. 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>
    
  6. 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>
    
  7. 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>
    
    
  8. 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>
    
  9. 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>
    
  10. 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>
    
  11. 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>
    
  12. 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>
    
  13. 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>
    
  14. 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>
    
  15. 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>
    
  16. 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 的移动应用的详细信息,请参阅: