5.3.1 示例代码

“5.3.1.1 创建内部帐户”是认证器应用的示例,“5.3.1.2 使用内部帐户”是请求应用的示例。 在 JSSEC 网站上分发的示例代码集中,每个代码集都对应账户管理器的认证器和用户。

5.3.1.1 创建内部账户

以下是认证器应用的示例代码,它使账户管理器能够使用内部帐户。 在此应用中没有可以从主屏幕启动的活动。 请注意,它间接通过账户管理器,从另一个示例代码“5.3.1.2 使用内部帐户”调用。

要点:

  1. 提供认证器的服务必须是私有的。
  2. 登录界面的活动必须在验证器应用中实现。
  3. 登录界面的活动必须实现为公共活动。
  4. 指定登录界面的活动的类名的显式意图,必须设置为KEY_INTENT
  5. 敏感信息(如帐户信息或认证令牌)不得输出到日志中。
  6. 密码不应保存在帐户管理器中。
  7. HTTPS 应该用于认证器与在线服务之间的通信。

提供认证器的账户管理器 IBinder 的服务,在AndroidManifest.xml中定义。 通过元数据指定编写认证器的资源XML文件。

账户管理器认证器/AndroidManifest.xml

  1. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  2. package="org.jssec.android.accountmanager.authenticator"
  3. xmlns:tools="http://schemas.android.com/tools">
  4. <!-- Necessary Permission to implement Authenticator -->
  5. <uses-permission android:name="android.permission.GET_ACCOUNTS" />
  6. <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
  7. <application
  8. android:allowBackup="false"
  9. android:icon="@drawable/ic_launcher"
  10. android:label="@string/app_name" >
  11. <!-- Service which gives IBinder of Authenticator to AccountManager -->
  12. <!-- *** POINT 1 *** The service that provides an authenticator must be private. -->
  13. <service
  14. android:name=".AuthenticationService"
  15. android:exported="false" >
  16. <!-- intent-filter and meta-data are usual pattern. -->
  17. <intent-filter>
  18. <action android:name="android.accounts.AccountAuthenticator" />
  19. </intent-filter>
  20. <meta-data
  21. android:name="android.accounts.AccountAuthenticator"
  22. android:resource="@xml/authenticator" />
  23. </service>
  24. <!-- Activity for for login screen which is displayed when adding an account -->
  25. <!-- *** POINT 2 *** The login screen activity must be implemented in an authenticator applicati
  26. on. -->
  27. <!-- *** POINT 3 *** The login screen activity must be made as a public activity. -->
  28. <activity
  29. android:name=".LoginActivity"
  30. android:exported="true"
  31. android:label="@string/login_activity_title"
  32. android:theme="@android:style/Theme.Dialog"
  33. tools:ignore="ExportedActivity" />
  34. </application>
  35. </manifest>

通过 XML 文件定义认证器,指定内部账户的账户类型以及其他。

res/xml/authenticator.xml

  1. <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:accountType="org.jssec.android.accountmanager"
  3. android:icon="@drawable/ic_launcher"
  4. android:label="@string/label"
  5. android:smallIcon="@drawable/ic_launcher"
  6. android:customTokens="true" />

AccountManager提供Authenticator实例的服务。 简单的实现返回JssecAuthenticator类的实例,它就是由onBind()在此示例中实现的Authenticator,这就足够了。

AuthenticationService.java

  1. package org.jssec.android.accountmanager.authenticator;
  2. import android.app.Service;
  3. import android.content.Intent;
  4. import android.os.IBinder;
  5. public class AuthenticationService extends Service {
  6. private JssecAuthenticator mAuthenticator;
  7. @Override
  8. public void onCreate() {
  9. mAuthenticator = new JssecAuthenticator(this);
  10. }
  11. @Override
  12. public IBinder onBind(Intent intent) {
  13. return mAuthenticator.getIBinder();
  14. }
  15. }

JssecAuthenticator是在此示例中实现的认证器。 它继承了AbstractAccountAuthenticator,并且实现了所有的抽象方法。 这些方法由账户管理器调用。 在addAccount()getAuthToken()中,用于启动LoginActivity,从在线服务中获取认证令牌的意图返回到账户管理器。

JssecAuthenticator.java

  1. package org.jssec.android.accountmanager.authenticator;
  2. import android.accounts.AbstractAccountAuthenticator;
  3. import android.accounts.Account;
  4. import android.accounts.AccountAuthenticatorResponse;
  5. import android.accounts.AccountManager;
  6. import android.accounts.NetworkErrorException;
  7. import android.content.Context;
  8. import android.content.Intent;
  9. import android.os.Bundle;
  10. public class JssecAuthenticator extends AbstractAccountAuthenticator {
  11. public static final String JSSEC_ACCOUNT_TYPE = "org.jssec.android.accountmanager";
  12. public static final String JSSEC_AUTHTOKEN_TYPE = "webservice";
  13. public static final String JSSEC_AUTHTOKEN_LABEL = "JSSEC Web Service";
  14. public static final String RE_AUTH_NAME = "reauth_name";
  15. protected final Context mContext;
  16. public JssecAuthenticator(Context context) {
  17. super(context);
  18. mContext = context;
  19. }
  20. @Override
  21. public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
  22. String authTokenType, String[] requiredFeatures, Bundle options)
  23. throws NetworkErrorException {
  24. AccountManager am = AccountManager.get(mContext);
  25. Account[] accounts = am.getAccountsByType(JSSEC_ACCOUNT_TYPE);
  26. Bundle bundle = new Bundle();
  27. if (accounts.length > 0) {
  28. // In this sample code, when an account already exists, consider it as an error.
  29. bundle.putString(AccountManager.KEY_ERROR_CODE, String.valueOf(-1));
  30. bundle.putString(AccountManager.KEY_ERROR_MESSAGE,
  31. mContext.getString(R.string.error_account_exists));
  32. } else {
  33. // *** POINT 2 *** The login screen activity must be implemented in an authenticator application.
  34. // *** POINT 4 *** The explicit intent which the class name of the login screen activity is specified must be set to KEY_INTENT.
  35. Intent intent = new Intent(mContext, LoginActivity.class);
  36. intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
  37. bundle.putParcelable(AccountManager.KEY_INTENT, intent);
  38. }
  39. return bundle;
  40. }
  41. @Override
  42. public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
  43. String authTokenType, Bundle options) throws NetworkErrorException {
  44. Bundle bundle = new Bundle();
  45. if (accountExist(account)) {
  46. // *** POINT 4 *** KEY_INTENT must be given an explicit intent that is specified the class name of the login screen activity.
  47. Intent intent = new Intent(mContext, LoginActivity.class);
  48. intent.putExtra(RE_AUTH_NAME, account.name);
  49. bundle.putParcelable(AccountManager.KEY_INTENT, intent);
  50. } else {
  51. // When the specified account doesn't exist, consider it as an error.
  52. bundle.putString(AccountManager.KEY_ERROR_CODE, String.valueOf(-2));
  53. bundle.putString(AccountManager.KEY_ERROR_MESSAGE,
  54. mContext.getString(R.string.error_account_not_exists));
  55. }
  56. return bundle;
  57. }
  58. @Override
  59. public String getAuthTokenLabel(String authTokenType) {
  60. return JSSEC_AUTHTOKEN_LABEL;
  61. }
  62. @Override
  63. public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
  64. Bundle options) throws NetworkErrorException {
  65. return null;
  66. }
  67. @Override
  68. public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
  69. return null;
  70. }
  71. @Override
  72. public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
  73. String authTokenType, Bundle options) throws NetworkErrorException {
  74. return null;
  75. }
  76. @Override
  77. public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
  78. String[] features) throws NetworkErrorException {
  79. Bundle result = new Bundle();
  80. result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
  81. return result;
  82. }
  83. private boolean accountExist(Account account) {
  84. AccountManager am = AccountManager.get(mContext);
  85. Account[] accounts = am.getAccountsByType(JSSEC_ACCOUNT_TYPE);
  86. for (Account ac : accounts) {
  87. if (ac.equals(account)) {
  88. return true;
  89. }
  90. }
  91. return false;
  92. }
  93. }

这是登录活动,它向在线服务发送帐户名称和密码,并执行登录认证,并因此获得认证令牌。 它会在添加新帐户或再次获取认证令牌时显示。 假设在线服务的实际访问在WebService类中实现。

LoginActivity.java

  1. package org.jssec.android.accountmanager.authenticator;
  2. import org.jssec.android.accountmanager.webservice.WebService;
  3. import android.accounts.Account;
  4. import android.accounts.AccountAuthenticatorActivity;
  5. import android.accounts.AccountManager;
  6. import android.content.Intent;
  7. import android.os.Bundle;
  8. import android.text.InputType;
  9. import android.text.TextUtils;
  10. import android.util.Log;
  11. import android.view.View;
  12. import android.view.Window;
  13. import android.widget.EditText;
  14. public class LoginActivity extends AccountAuthenticatorActivity {
  15. private static final String TAG = AccountAuthenticatorActivity.class.getSimpleName();
  16. private String mReAuthName = null;
  17. private EditText mNameEdit = null;
  18. private EditText mPassEdit = null;
  19. @Override
  20. public void onCreate(Bundle icicle) {
  21. super.onCreate(icicle);
  22. // Display alert icon
  23. requestWindowFeature(Window.FEATURE_LEFT_ICON);
  24. setContentView(R.layout.login_activity);
  25. getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
  26. android.R.drawable.ic_dialog_alert);
  27. // Find a widget in advance
  28. mNameEdit = (EditText) findViewById(R.id.username_edit);
  29. mPassEdit = (EditText) findViewById(R.id.password_edit);
  30. // *** POINT 3 *** The login screen activity must be made as a public activity, and suppose the attack access from other application.
  31. // Regarding external input, only RE_AUTH_NAME which is String type of Intent#extras, are handled.
  32. // This external input String is passed toextEdit#setText(), WebService#login(),new Account(),
  33. // as a parameter,it's verified that there's no problem if any character string is passed.
  34. mReAuthName = getIntent().getStringExtra(JssecAuthenticator.RE_AUTH_NAME);
  35. if (mReAuthName != null) {
  36. // Since LoginActivity is called with the specified user name, user name should not be editable.
  37. mNameEdit.setText(mReAuthName);
  38. mNameEdit.setInputType(InputType.TYPE_NULL);
  39. mNameEdit.setFocusable(false);
  40. mNameEdit.setEnabled(false);
  41. }
  42. }
  43. // It's executed when login button is pressed.
  44. public void handleLogin(View view) {
  45. String name = mNameEdit.getText().toString();
  46. String pass = mPassEdit.getText().toString();
  47. if (TextUtils.isEmpty(name) || TextUtils.isEmpty(pass)) {
  48. // Process when the inputed value is incorrect
  49. setResult(RESULT_CANCELED);
  50. finish();
  51. }
  52. // Login to online service based on the inpputted account information.
  53. WebService web = new WebService();
  54. String authToken = web.login(name, pass);
  55. if (TextUtils.isEmpty(authToken)) {
  56. // Process when authentication failed
  57. setResult(RESULT_CANCELED);
  58. finish();
  59. }
  60. // Process when login was successful, is as per below.
  61. // *** POINT 5 *** Sensitive information (like account information or authentication token) must not be output to the log.
  62. Log.i(TAG, "WebService login succeeded");
  63. if (mReAuthName == null) {
  64. // Register accounts which logged in successfully, to aAccountManager
  65. // *** POINT 6 *** Password should not be saved in Account Manager.
  66. AccountManager am = AccountManager.get(this);
  67. Account account = new Account(name, JssecAuthenticator.JSSEC_ACCOUNT_TYPE);
  68. am.addAccountExplicitly(account, null, null);
  69. am.setAuthToken(account, JssecAuthenticator.JSSEC_AUTHTOKEN_TYPE, authToken);
  70. Intent intent = new Intent();
  71. intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, name);
  72. intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE,
  73. JssecAuthenticator.JSSEC_ACCOUNT_TYPE);
  74. setAccountAuthenticatorResult(intent.getExtras());
  75. setResult(RESULT_OK, intent);
  76. } else {
  77. // Return authentication token
  78. Bundle bundle = new Bundle();
  79. bundle.putString(AccountManager.KEY_ACCOUNT_NAME, name);
  80. bundle.putString(AccountManager.KEY_ACCOUNT_TYPE,
  81. JssecAuthenticator.JSSEC_ACCOUNT_TYPE);
  82. bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
  83. setAccountAuthenticatorResult(bundle);
  84. setResult(RESULT_OK);
  85. }
  86. finish();
  87. }
  88. }

实际上,WebService类在这里是虚拟实现,这是假设认证总是成功的示例实现,并且固定字符串作为认证令牌返回。

WebService.java

  1. package org.jssec.android.accountmanager.webservice;
  2. public class WebService {
  3. /**
  4. * Suppose to access to account managemnet function of online service.
  5. *
  6. * @param username Account name character string
  7. * @param password password character string
  8. * @return Return authentication token
  9. */
  10. public String login(String username, String password) {
  11. // *** POINT 7 *** HTTPS should be used for communication between an authenticator and the online services.
  12. // Actually, communication process with servers is implemented here, but Omit here, since this is a sample.
  13. return getAuthToken(username, password);
  14. }
  15. private String getAuthToken(String username, String password) {
  16. // In fact, get the value which uniqueness and impossibility of speculation are guaranteed by the server,
  17. // but the fixed value is returned without communication here, since this is sample.
  18. return "c2f981bda5f34f90c0419e171f60f45c";
  19. }
  20. }

5.3.1.2 使用内部账户

以下是应用示例代码,它添加内部帐户并获取认证令牌。 当另一个示例应用“5.3.1.1 创建内部帐户”安装在设备上时,可以添加内部帐户或获取认证令牌。 仅当两个应用的签名密钥不同时,才会显示“访问请求”界面。

5.3.1.md - 图1

要点:

在验证认证器是否正常之后,执行账户流程。

AccountManager用户应用的AndroidManifest.xml。 声明使用必要的权限。请参阅“5.3.3.1 账户管理器和权限的使用”来了解必要的权限。

账户管理器用户/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.jssec.android.accountmanager.user" >
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <application
        android:allowBackup="false"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".UserActivity"
            android:label="@string/app_name"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

用户应用的活动。 当点击屏幕上的按钮时,会执行addAccount()getAuthToken()。 在某些情况下,对应特定帐户类型的认证器可能是伪造的,因此请注意在验证认证器正常后,启动帐户流程。

UserActivity.java

package org.jssec.android.accountmanager.user;

import java.io.IOException;
import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.Utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorDescription;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class UserActivity extends Activity {

    // Information of the Authenticator to be used
    private static final String JSSEC_ACCOUNT_TYPE = "org.jssec.android.accountmanager";
    private static final String JSSEC_TOKEN_TYPE = "webservice";
    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_activity);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    public void addAccount(View view) {
        logLine();
        logLine("Add a new account");
        // *** POINT 1 *** Execute the account process after verifying if the authenticator is regular one.
        if (!checkAuthenticator()) return;
        AccountManager am = AccountManager.get(this);
        am.addAccount(JSSEC_ACCOUNT_TYPE, JSSEC_TOKEN_TYPE, null, null, this,
            new AccountManagerCallback<Bundle>() {

                @Override
                public void run(AccountManagerFuture<Bundle> future) {
                    try {
                        Bundle result = future.getResult();
                        String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
                        String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);
                        if (type != null && name != null) {
                            logLine("Add the following accounts:");
                            logLine(" Account type: %s", type);
                            logLine(" Account name: %s", name);
                        } else {
                            String code = result.getString(AccountManager.KEY_ERROR_CODE);
                            String msg = result.getString(AccountManager.KEY_ERROR_MESSAGE);
                            logLine("The account cannot be added");
                            logLine(" Error code %s: %s", code, msg);
                        }
                    } catch (OperationCanceledException e) {
                    } catch (AuthenticatorException e) {
                    } catch (IOException e) {
                    }
                }
            }, null);
    }

    public void getAuthToken(View view) {
        logLine();
        logLine("Get token");
        // *** POINT 1 *** After checking that the Authenticator is the regular one, execute account process.
        if (!checkAuthenticator()) return;
        AccountManager am = AccountManager.get(this);
        Account[] accounts = am.getAccountsByType(JSSEC_ACCOUNT_TYPE);
        if (accounts.length > 0) {
            Account account = accounts[0];
            am.getAuthToken(account, JSSEC_TOKEN_TYPE, null, this,
                new AccountManagerCallback<Bundle>() {

                    @Override
                    public void run(AccountManagerFuture<Bundle> future) {
                        try {
                            Bundle result = future.getResult();
                            String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);
                            String authtoken = result.getString(AccountManager.KEY_AUTHTOKEN);
                            logLine("%s-san's token:", name);
                            if (authtoken != null) {
                                logLine(" %s", authtoken);
                            } else {
                                logLine(" Couldn't get");
                            }
                        } catch (OperationCanceledException e) {
                            logLine(" Exception: %s",e.getClass().getName());
                        } catch (AuthenticatorException e) {
                            logLine(" Exception: %s",e.getClass().getName());
                        } catch (IOException e) {
                            logLine(" Exception: %s",e.getClass().getName());
                        }
                    }
                }, null);
        } else {
            logLine("Account is not registered.");
        }
    }

    // *** POINT 1 *** Verify that Authenticator is regular one.
    private boolean checkAuthenticator() {
        AccountManager am = AccountManager.get(this);
        String pkgname = null;
        for (AuthenticatorDescription ad : am.getAuthenticatorTypes()) {
            if (JSSEC_ACCOUNT_TYPE.equals(ad.type)) {
                pkgname = ad.packageName;
                break;
            }
        }
        if (pkgname == null) {
            logLine("Authenticator cannot be found.");
            return false;
        }
        logLine(" Account type: %s", JSSEC_ACCOUNT_TYPE);
        logLine(" Package name of Authenticator: ");
        logLine(" %s", pkgname);
        if (!PkgCert.test(this, pkgname, getTrustedCertificateHash(this))) {
            logLine(" It's not regular Authenticator(certificate is not matched.)");
            return false;
        }
        logLine(" This is regular Authenticator.");
        return true;
    }

    // Certificate hash value of regular Authenticator application
    // Certificate hash value can be checked in sample applciation JSSEC CertHash Checker
    private String getTrustedCertificateHash(Context context) {
        if (Utils.isDebuggable(context)) {
            // Certificate hash value of debug.keystore "androiddebugkey"
            return "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
        } else {
            // Certificate hash value of keystore "my company key"
            return "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
        }
    }

    private void log(String str) {
        mLogView.append(str);
    }

    private void logLine(String line) {
        log(line + "¥n");
    }

    private void logLine(String fmt, Object... args) {
        logLine(String.format(fmt, args));
    }

    private void logLine() {
        log("¥n");
    }
}

PkgCert.java

package org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            if (pkginfo.signatures.length != 1) return null; // Will not handle multiple signatures.
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}