Android 登录 - 帐户验证器与手动验证

Android Login - Account Authenticator vs Manual Authentication

我即将在我的应用程序中实现登录和用户身份验证。

我的第一个想法是手动完成,向服务器注册用户名和密码,获取授权令牌,保存并在后续请求中使用它。

谷歌搜索后,我了解到在 Android 上执行此操作的正确方法是使用帐户验证器。我看过它的一些实施示例,但我不明白这样做的好处?是因为我可以存储多个帐户吗?是因为同步问题吗?如果有人可以向我解释这一点,我将不胜感激。它可能会让我更好地理解它的代码以及它为什么这样做。

I can have more than one account stored?

是的。看看 GoogleFacebook 是怎么做的。

Is it because of syncing issues?

是的,您需要帐户才能使用 SyncAdapter

等同步机制

为什么要使用 AccountAuthenticator

  • 支持后台同步机制如SyncAdapter;

  • 验证用户的标准方法;

  • 支持不同的令牌;

  • 不同权限的账户共享

你需要做什么?

1).创建 Authenticator;

2).为用户登录创建 Activity

3).创建 Service 以与帐户通信。

条款。

AccountManager - 它管理设备上的帐户。请求您应该使用的身份验证令牌 AccountManager.

AbstractAccountAuthenticator - 用于处理帐户类型的组件。它包含使用帐户(授权、访问权限等)的所有逻辑。一个 AbstractAccountAuthenticator 可以被不同的应用程序使用(例如 Google Gmail、日历、Drive 等帐户)

AccountAuthenticatorActivity - 基础 Activity,用于 authorize/create 帐户。 AccountManager 如果需要识别账户(Token 不存在或过期)则调用此账户

这一切是如何运作的?看下图:

步数。

1).创建 Authenticator;

您需要扩展 AbstractAccountAuthenticator 并覆盖 7 个方法:

  • Bundle editProperties(AccountAuthenticatorResponse response, String accountType) link
  • Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) link
  • Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) link
  • Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) link
  • String getAuthTokenLabel(String authTokenType) link
  • Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) link
  • Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) link

示例:

public class LodossAuthenticator extends AbstractAccountAuthenticator {

    private static final String LOG_TAG = LodossAuthenticator.class.getSimpleName();

    private final Context mContext;

    public LodossAuthenticator(Context context) {
        super(context);
        mContext = context;
    }

    @Override
    public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
        return null;
    }

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
        final Intent intent = new Intent(mContext, CustomServerAuthenticatorSigninActivity.class);
        intent.putExtra(Config.ARG_ACCOUNT_TYPE, accountType);
        intent.putExtra(Config.ARG_AUTH_TYPE, authTokenType);
        intent.putExtra(Config.ARG_IS_ADDING_NEW_ACCOUNT, true);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

        final Bundle bundle = new Bundle();
    bundle.putParcelable(AccountManager.KEY_INTENT, intent);
    return bundle;
    }

    @Override
    public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
        // If the caller requested an authToken type we don't support, then
        // return an error
        if (!authTokenType.equals(AccountGeneral.AUTHTOKEN_TYPE_READ_ONLY) && !authTokenType.equals(AccountGeneral.AUTHTOKEN_TYPE_FULL_ACCESS)) {
            final Bundle result = new Bundle();
            result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
            return result;
        }

        // Extract the username and password from the Account Manager, and ask
        // the server for an appropriate AuthToken.
        final AccountManager am = AccountManager.get(mContext);
        String authToken = am.peekAuthToken(account, authTokenType);

        // Lets give another try to authenticate the user
        if (TextUtils.isEmpty(authToken)) {
            final String password = am.getPassword(account);
            if (password != null) {
                try {
                    authToken = sServerAuthenticate.userSignIn(account.name, password, authTokenType);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        // If we get an authToken - we return it
        if (!TextUtils.isEmpty(authToken)) {
            final Bundle result = new Bundle();
            result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
            result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
            result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
            return result;
        }

        // If we get here, then we couldn't access the user's password - so we
        // need to re-prompt them for their credentials. We do that by creating
        // an intent to display our AuthenticatorActivity.
        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        intent.putExtra(com.lodoss.authlib.Config.ARG_ACCOUNT_TYPE, account.type);
        intent.putExtra(com.lodoss.authlib.Config.ARG_AUTH_TYPE, authTokenType);
        intent.putExtra(Config.ARG_ACCOUNT_NAME, account.name);
        final Bundle bundle = new Bundle();
    bundle.putParcelable(AccountManager.KEY_INTENT, intent);
    return bundle;
    }

    @Override
    public String getAuthTokenLabel(String authTokenType) {
        if (AccountGeneral.AUTHTOKEN_TYPE_FULL_ACCESS.equals(authTokenType))
            return AccountGeneral.AUTHTOKEN_TYPE_FULL_ACCESS_LABEL;
        else if (AccountGeneral.AUTHTOKEN_TYPE_READ_ONLY.equals(authTokenType))
            return AccountGeneral.AUTHTOKEN_TYPE_READ_ONLY_LABEL;
        else
            return authTokenType + " (Label)";
    }

    @Override
    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
        final Bundle result = new Bundle();
        result.putBoolean(KEY_BOOLEAN_RESULT, false);
        return result;
    }
}

解释:

因此,您只需要查看 2 个方法:addAccountgetAuthToken

addAccount 中,我添加了一些配置参数,我的 Activity 将使用这些参数进行用户登录。这里的要点是 intent.putExtra(Config.ARG_ACCOUNT_TYPE, accountType); - 您应该在此处指定帐户类型。不需要其他操作。

getAuthToken - 请阅读评论。我从 UdinicAuthenticator.java

复制粘贴了这个方法

此外,您需要在 AndroidManifest.xml 中获得以下权限:

<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />

方法摘要 addAccountgetAuthToken

尝试获取令牌,如果令牌存在return结果,否则你会看到Activity授权

2).为用户登录创建 Activity

AuthenticatorActivity

简要说明: 使用 UserId 和密码创建表单。使用用户 ID 和密码数据从服务器获取身份验证令牌,然后执行以下步骤:

mAccountManager.addAccountExplicitly(account, accountPassword, null);
mAccountManager.setAuthToken(account, authtokenType, authtoken);

3).创建一个 Service 与帐户通信。

UdinicAuthenticatorService

不要忘记将 AndroidManifest.xml 中的这一行添加到 Service:

    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator" />
    </intent-filter>
    <meta-data android:name="android.accounts.AccountAuthenticator"
               android:resource="@xml/authenticator" />

并且还在 res/xml 添加文件 authenticator.xml:

<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
                       android:accountType="com.mediamanagment.app"
                       android:icon="@drawable/ic_launcher"
                       android:smallIcon="@drawable/ic_launcher"
                       android:label="@string/authenticator_label"/>

就是这样。您可以使用 AccountAuthenticator.

来源材料感谢

AccountManager 很好,原因如下:

  • 首先是在单一帐户类型下存储多个帐户名称,这些帐户名称对应用程序的功能具有不同级别的访问权限。例如,在一个视频流应用程序中,一个人可能有两个帐户名:一个具有对有限数量视频的演示访问权限,另一个具有对所有视频的完整月访问权限。然而,这不是使用 Accounts 的主要原因,因为您可以在您的应用程序中轻松管理它,而不需要这个看起来很花哨的 Accounts 东西......
  • 使用 Accounts 的另一个优点是摆脱了每次用户请求授权功能时使用用户名和密码的传统授权,因为身份验证在后台进行,用户是仅在某些情况下要求他们提供密码,稍后我会讲到。
  • 使用 android 中的 Accounts 功能也无需定义自己的帐户类型。您可能遇到过使用 Google 帐户进行授权的应用程序,这省去了创建新帐户并为用户记住其凭据的麻烦。
  • Accounts可通过设置→账户独立添加
  • 跨平台用户授权可以使用Accounts轻松管理。例如,客户端可以在其 android 设备和 PC 中同时访问受保护的 material,而无需重复登录。
  • 从安全的角度来看,在对服务器的每个请求中使用相同的密码允许在非安全连接中进行可能的窃听。此处密码加密不足以防止密码被盗
  • 最后,在android中使用Accounts特性的一个重要原因是将依赖Accounts的任何业务所涉及的两方分开,即认证者和资源所有者,在不损害客户端(用户)凭据的情况下。这些术语可能看起来很模糊,但在阅读以下段落之前不要放弃......

让我以视频流应用程序为例详细说明后者。 A 公司是视频流媒体业务的持有人,与 B 公司签订合同,为其某些成员提供优质流媒体服务。 B 公司采用用户名和密码的方法来识别其用户。 A 公司要认可 B 的高级会员,一种方法是从 B 那里获取他们的名单,并利用类似 username/password 的匹配机制。这样,身份验证者和资源所有者是相同的(公司 A)。除了用户有义务记住第二个密码外,很可能他们为使用A的服务而设置了与B公司的个人资料相同的密码。这显然是不利的。

为了弥补上述缺点,引入了OAuth。作为授权的开放标准,在上面的示例中,OAuth 要求授权由 B 公司(身份验证者)通过为符合条件的用户(第三方)颁发一些称为访问令牌的令牌,然后向公司 A(资源所有者)提供令牌。所以没有令牌意味着没有资格。

我在我的网站 here

上的 AccountManager 上对此进行了详细说明。

在 android 的设置中,您有适合您的帐户类型的帐户,您可以从那里添加一个帐户。 AccountManager 也是存储凭据的中心位置,因此您只需为每个供应商登录一次。如果您下载另一个 google 应用程序或多次访问一个应用程序,您只需输入一次凭据