使用者帳號服務。
你可以以你的 app 名義透過 AccountManager 建立屬於你的使用者。
建立帳號(Top Down):
accountManager.addAccountExplicitly((Account) account, "password", (Bundle) userdata); // nullable userdata
Account account = new Account(username, accountType);
String accountType = "com.infstory"; // 帳號識別類型
String username = "[email protected]"; // 帳號名稱
相當然爾,你可以提供登入畫面,透過網路服務確定登入成功後,再執行上方程式來建立本地使用者。個資以及帳密都會存在系統裡,僅有 app 擁有者本身才可以存取密碼。(當然已經鮮少 app 會頻繁的使用密碼,一般後端的使用者授權服務,大多登入成功後,會提供一組暫時授權證書,大多操作皆使用此授權證書)
從系統中取得帳號資料(userdata, 帳密與授權證書需要 app 擁有者才可以存取):
Account[] accounts = accountManager.getAccountsByType(accountType);
Account account = accounts.length != 0 ? accounts[0];
設定授權證書(accessToken/authToken):
accountManager.setAuthToken(account, authTokenType, authToken);
取得授權證書(accessToken/authToken):
String authToken = accountManager.peekAuthToken(account, authTokenType)
我們可以寫一個 GitHub App ,GitHub 網頁登入後,建立一個 GitHub 帳號,把 access token 存進去。
public class LoginActivity extends Activity {
@Override public void onResume() {
super.onResume();
loginButton.onClick(() -> {
final GitHub github = GitHub.create();
// Populate username, password, clientId, clientSecret from UI
// ..
AppObservable.bindActivity(this, github.getAccessToken(username, password, clientId, clientSecret))
.subscribeOn(Scheduler.io())
.subscribe(accessToken -> {
String accountType = "com.github";
Account account = new Account(username, "com.github");
AccountManager accountManager = AccountManager.get(context);
accountManager.addAccountExplicitly(account, password, userdata);
// 一般會寫 "user,user:email" 等,在 oauth 稱為 scope, 不過帳密授權應該就全部權限,這邊特別寫明是密碼登入,方便識別。
String authType = "password"; // authType/scope/permissions
String authToken = accessToken.accessToken;
accountManager.setAuthToken(account, authType, authToken);
finish();
});
});
}
}
@Retrofit("https://api.github.com")
public abstract class GitHub {
@FormUrlEncoded
@POST("/oauth/token?grant_type=password") Observable<AccessToken> getAccessToken(
@Field("username") String email,
@Field("password") String password,
@Field("client_id") String clientId,
@Field("client_secret") String clientSecret);
public static GitHub create() { return new Retrofit_GitHub(); }
}
3rd-party app 只要依據你帳號管理中心的 accountType 作為使用者中心/授權服務的識別名稱,呼叫即可透過系統叫起你的登入畫面,以提供授權證書。
AccountManagerFuture<Bundle> authTokenFutureBundle = accountManager.getAuthToken(account, authTokenType, (Bundle) null, activity, (AccountManagerCallback<Bundle>) null, (Handler) null);
String authToken = authTokenFutureBundle.getString(AccountManager.KEY_AUTHTOKEN); // waiting for UI logon
GitHub App 就要比較辛苦的扮演使用者中心:
- 提供帳號管理中心
- 以及其登入畫面
- 向 AccountManager 建立使用者
先從剛熟悉的登入畫面開始,不只是單純的自己登入就好,要想辦法把 authToken 傳給委託人。
改繼承 AccountAuthenticatorActivity 讓它幫你把一些你不想知道的東西塞好,如果你是用 AppCompatActivity 之類的,請參考 AccountAuthenticatorActivity.java 源碼,自己塞。
public class LoginActivity extends AccountAuthenticatorActivity { // 1.
@Override public void onResume() {
super.onResume();
loginButton.onClick(() -> {
final GitHub github = GitHub.create();
// ..
AppObservable.bindActivity(this, github.getAccessToken(username, password, clientId, clientSecret))
.subscribeOn(Scheduler.io())
.subscribe(accessToken -> {
String accountType = "com.github";
Account account = new Account(username, "com.github");
AccountManager accountManager = AccountManager.get(context);
accountManager.addAccountExplicitly(account, password, userdata);
String authType = "password";
String authToken = accessToken.accessToken;
accountManager.setAuthToken(account, authType, authToken);
// 2.
Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, username);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType);
intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken);
((AccountAuthenticatorActivity) this).setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
});
});
}
}
委託登入授權服務(Authenticator) 實現所有 AccountManager 相關的代理操作。
3rd-party 對 AccountManager.getAuthToken() 其實會委託給我們的登入授權服務 Authenticator 受理 ,這部份我們再叫出剛配置好的登入畫面:
// 登入授權服務
public class GitHubAuthenticator extends AbstractAccountAuthenticator {
// 取得授權
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle options) throws NetworkErrorException {
// 1. 你可以無條件提供授權
// 2. 或者你可以提供一個畫面提醒使用者,是否同意提供授權
// 如果還沒登入請使用者登入完再跳回來,繼續授權。
AccountManager accountManager = AccountManager.get(context);
// 拿拿看既有 GitHub Service 的權限授權
String authToken = accountManager.peekAuthToken(account, authTokenType);
String accountType = "com.github";
if (authToken != null) {
Bundle bundle = new Bundle();
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
return bundle;
}
String authType = "password";
// 拿不到 authToken 就當作登入吧 (由於目前我們的政策是,登入後,授權已經塞好在 bundle 了,所以我們可以直接回傳)
return addAccount(response, accountType, authType, (String[]) null, (Bundle) null);
}
// 建立帳號
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
String authTokenType, String[] requiredFeatures, Bundle options)
throws NetworkErrorException {
// 登入畫面
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
}
公開授權服務 AndroidManifest.xml:
<service
android:name=".GitHubAuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<activity
android:name=".LoginActivity"
android:exported="true"
android:label="@string/app_name" />
res/xml/authenticator.xml:
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.github"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/account_label" />
為了聽 AccountManagerService 發出來的委託 Service:
public class GitHubAuthenticatorService extends Service {
@Override
public IBinder onBind(Intent intent) {
return new GitHubAuthenticator(this).getIBinder();
}
}
GitHub github = GitHub.create(context);
github.repos().subscribe(System.out::println);
public interface GitHub {
@RequestInterceptor(GitHubAuthInterceptor.class)
@GET("/{owner}/repos")
Observable<Repo> repos(@Path String owner);
}
在呼叫 GitHub.repos()
時,會先呼叫 accountManager.getAuthToken()
,拿到 token 後,才憑證發出 GET /{owner}/repos
request 。
@Singleton
public class GitHubAuthInterceptor extends AuthenticationInterceptor {
@Override
public String accountType() {
return "com.github";
}
@Override
public String authTokenType() {
return "com.github";
}
@Override
public void intercept(String token, RequestFacade request) {
if (token != null) request.addHeader("Authorization", "Bearer " + token);
}
}
@Authentication(accountType = R.string.auth_account_type, tokenType = R.string.auth_token_type)
public interface GitHub {
@Authenticated
@GET("/{owner}/repos")
Observable<Repo> repos(@Path String owner);
}
getAccountName()
.flatMap(this::getAccount)
.flatMap(this::getAuthToken) // 取得授權證
.flatMap(this::authenticate) // 設定憑證
.flatMap(o -> request)
.retry((c, e) -> retryRule.retry(c, e));