Android開發案例 - 優雅地強制用戶從新登陸

大部分移動應用都是須要使用帳號登陸才能使用的。既然有帳號登陸操做,那麼相應地也有帳號登出操做。帳號登出包含如下兩種狀況:java

  1. 用戶主動登出帳號
  2. 用戶被迫登出帳號

第一種狀況實現相對比較簡單,本文暫不涉及。
第二種狀況主要是因爲登陸憑證(AccessToken)過時失效等緣由,致使應用強制登出,而且要求用戶從新登陸。android

在文章開始以前,先看看Android Q的新特性:git


官方資料 - 1:Android Q 隱私權變動:針對後臺 Activity 啓動的限制github

Android Q 對應用可啓動 Activity 的時間施加了限制。此項行爲變動有助於最大限度地減小對用戶形成的中斷,而且可讓用戶更好地控制其屏幕上顯示的內容。具體而言,在 Android Q 上運行的應用只有在知足如下一個或多個條件時才能啓動 Activity:服務器

  1. 該應用具備可見窗口,例如在前臺運行的 Activity。
  2. 在前臺運行的另外一個應用會發送屬於該應用的PendingIntent。示例包括髮送菜單項待定 intent 的自定義標籤頁提供程序。
  3. 系統發送屬於該應用的PendingIntent,例如點按通知。只有應用應啓動界面的待定 intent 才能夠免除。
  4. 系統嚮應用發送廣播,例如SECRET_CODE_ACTION。只有應用應啓動界面的特定廣播才能夠免除。

注意:出於 Activity 啓動的目的,前臺服務不會將應用限定爲在前臺運行。ide

此項行爲變動適用於在 Android Q 上運行的全部應用,包括以 Android 9(API 級別 28)或更低版本爲目標平臺的應用。此外,即便您的應用以 Android 9 或更低版本爲目標平臺而且最初安裝在運行 Android 9 或更低版本的設備上,該行爲變動仍會在設備升級到 Android Q 後生效。函數

可是,只要您的應用啓動 Activity 是因用戶互動直接引起的,該應用就極有可能不會受到此項變動的影響。實際上,大多數應用都不會受到此項變動的影響。若是您發現本身的應用受到了影響,請向咱們發送反饋。ui

官方資料 - 2:Broadcasts overviewthis

Security considerations and best practices
Here are some security considerations and best practices for sending and receiving broadcasts:
...rest

  • Do not start activities from broadcast receivers because the user experience is jarring; especially if there is more than one receiver. Instead, consider displaying a notification.

知識要點

  1. Android API Level 14
  2. ActivityLifecycleCallbacks
  3. BroadcastReceiver
  4. PendingIntent
  5. Intent.makeRestartActivityTask(ComponentName)

基本思路

  1. App註冊「強制登出」的私有廣播,且將優先級設置爲最低-999;
  2. App註冊ActivityLifecycleCallbacks,監聽Activity的生命週期;
  3. App#onActivityResumed(Activity)被調用時,將Activity註冊「強制登出」的私有廣播。此時,若是有「強制登出」的廣播發送,且應用在前臺運行,那麼則是前臺Activity先接收到此廣播,而後重啓頁面;應用在後臺運行,那麼此廣播就會被App接收到,而後下次打開應用時重啓頁面。
注意:「強制登出」的廣播必需要定義爲私有廣播,只能在應用內發送接收。

實現代碼

  • 變量和接口說明

1.YOUR_PACKAGE_NAME:項目工程的PackageName包名,即AndroidManifest.xml中的manifest-package值

<manifest package="${YOUR_PACKAGE_NAME}">

2.YOUR_LAUNCHER_ACTIVITY:啓動頁面。須要注意的是重啓界面時,會傳遞EXTRA_FORCE_LOGOUT_INTENT參數,告知啓動頁面進行必要的清理工做,如移除已登陸帳號信息等;
3.AnonymouslyAccessible:接口,表示不要求登陸就能夠打開的頁面;
4.isServerSettingsAcquired():服務器配置已配置,若是服務器地址不是固定域名,則須要實現此函數;
5.hasAuthenticatedUser():是否用戶已登陸。

根據實際項目工程替換掉YOUR_PACKAGE_NAMEYOUR_LAUNCHER_ACTIVITY變量,並填充isServerSettingsAcquired()hasAuthenticatedUser()

  • App.java:
public class App extends Application implements ActivityLifecycleCallbacks {
    public static String ACTION_FORCE_LOGOUT = "${YOUR_PACKAGE_NAME}.action.FORCE_LOGOUT";
    public static String PERMISSION_PRIVATE = "${YOUR_PACKAGE_NAME}.permission.PRIVATE";

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(this);

        IntentFilter intentFilter = new IntentFilter(ACTION_FORCE_LOGOUT);
        intentFilter.setPriority(-999);
        registerReceiver(mReceiver, intentFilter, PERMISSION_PRIVATE, null);
    }

    private boolean mWillForceLogout;

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            PendingIntent pendingIntent = getForceLogoutBroadcast(context, false);
            if (pendingIntent != null) {
                pendingIntent.cancel();
                mWillForceLogout = true;
            }
        }
    };

    private static PendingIntent getForceLogoutBroadcast(Context context, boolean createIfNotExist) {
        Intent intent = new Intent(ACTION_FORCE_LOGOUT);
        intent.setPackage(context.getPackageName());
        int flag = createIfNotExist ? 0 : PendingIntent.FLAG_NO_CREATE;
        return PendingIntent.getBroadcast(context, 0, intent, flag);
    }

    @Override
    public void onActivityResumed(Activity activity) {
        if (!(activity instanceof AnonymouslyAccessible)) {
            IntentFilter intentFilter = new IntentFilter(ACTION_FORCE_LOGOUT);
            activity.registerReceiver(mActivityReceiver, intentFilter, PERMISSION_PRIVATE, null);
        }

        boolean willForceLogout = takeWillForceLogout();
        if (willForceLogout || shouldStartLauncherActivity(activity)) {
            restartActivity(willForceLogout);
        }
    }

    protected void restartActivity(boolean willForceLogout) {
        ComponentName componentName = new ComponentName(this, ${YOUR_LAUNCHER_ACTIVITY}.class);
        Intent intent = Intent.makeRestartActivityTask(componentName);
        if (willForceLogout) {
            intent.putExtra(${YOUR_LAUNCHER_ACTIVITY}.EXTRA_FORCE_LOGOUT_INTENT, true);
        }
        startActivity(intent);
    }

    private BroadcastReceiver mActivityReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            restartActivity(true);
            PendingIntent pendingIntent = getForceLogoutBroadcast(context, false);
            if (pendingIntent != null) {
                pendingIntent.cancel();
            }
        }
    };

    private boolean takeWillForceLogout() {
        boolean willForceLogout = mWillForceLogout;
        mWillForceLogout = false;
        return willForceLogout;
    }

    private boolean shouldStartLauncherActivity(Activity activity) {
        if (activity instanceof AnonymouslyAccessible) {
            return false;
        }

        return !isServerSettingsAcquired() || !hasAuthenticatedUser();
    }

    public boolean isServerSettingsAcquired() {
        // YOUR CODE ?
    }

    public boolean hasAuthenticatedUser() {
        // YOUR CODE ?
    }

    @Override
    public void onActivityPaused(Activity activity) {
        if (!(activity instanceof AnonymouslyAccessible)) {
            activity.unregisterReceiver(mActivityReceiver);
        }
    }

    @Override
    public void onTerminate() {
        unregisterReceiver(mReceiver);
        unregisterActivityLifecycleCallbacks(this);
        super.onTerminate();
    }

    public static void forceLogout(Context context) {
        try {
            PendingIntent pendingIntent = getForceLogoutBroadcast(context, false);
            if (pendingIntent == null) {
                pendingIntent = getForceLogoutBroadcast(context, true);
                pendingIntent.send(context, 0, null, null, null, PERMISSION_PRIVATE);
            }
        } catch (CanceledException e) {
            e.printStackTrace();
        }
    }
}
  • AndroidManifest.xml:
<permission android:name="${YOUR_PACKAGE_NAME}.permission.PRIVATE"
    android:protectionLevel="signature" />
<uses-permission android:name="${YOUR_PACKAGE_NAME}.permission.PRIVATE" />

通常來講,客戶端收到登陸憑證(AccessToken)過時通知有兩種途徑:

  1. 頁面主動調用服務器接口,接口返回報錯通知登陸憑證過時
  2. 長鏈接推送登陸憑證過時通知

客戶端收到上述通知後,調用如下代碼便可:

App.forceLogout(Context)

END. >> SEE MORE: http://erehmi.github.io/

相關文章
相關標籤/搜索