likes
comments
collection
share

Android 中登录态保持的策略

作者站长头像
站长
· 阅读数 10

思维导图

Android 中登录态保持的策略

在Android中模仿切换角色时保持登录状态,可以采用以下几种方法:

1. 使用SharedPreferences

SharedPreferences是Android提供的一种轻量级的数据存储方式,适合用于保存一些简单的配置信息或状态,例如用户的登录状态。

当用户登录成功后,可以调用saveLoginStatus方法将登录状态保存到SharedPreferences中。当用户切换角色并重新进入应用时,可以调用getLoginStatus方法从SharedPreferences中读取登录状态,并根据返回的值来恢复用户的登录状态。

需要注意的是,在使用SharedPreferences保存敏感信息时,应该对数据进行加密处理,以确保安全性。

此外,还需要考虑到异常情况的处理,例如用户登出或者长时间未操作导致的自动退出登录。在设计时,应该结合具体的应用场景和用户需求,选择最合适的方法来实现保持登录状态的功能。

package com.login;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * 模仿切换角色时保持登录状态
 */
public class LoginManager {

    private static final String PREF_NAME = "login_status";
    private static final String KEY_IS_LOGGED_IN = "is_logged_in";

    private Context context;

    public LoginManager(Context context) {
        this.context = context;
    }

    /**
     * 使用SharedPreferences:SharedPreferences是Android提供的一种轻量级的数据存储方式,适合用于保存一些简单的配置
     * 信息或状态,例如用户的登录状态。
     * @param isLoggedIn
     */
    public void saveLoginStatus(boolean isLoggedIn) {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
//        editor.putBoolean(KEY_IS_LOGGED_IN, isLoggedIn);
        String encryptedStatus = EncryptionUtils.encrypt(String.valueOf(isLoggedIn));
        editor.putString(KEY_IS_LOGGED_IN, encryptedStatus);
        editor.apply();
    }

    public boolean getLoginStatus() {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
//        return sharedPreferences.getBoolean(KEY_IS_LOGGED_IN, false);
        String encryptedStatus = sharedPreferences.getString(KEY_IS_LOGGED_IN, null);
        if (encryptedStatus != null) {
            String decryptedStatus = EncryptionUtils.decrypt(encryptedStatus);
            return Boolean.parseBoolean(decryptedStatus);
        }
        return false;
    }

    /**
     * 加密敏感数据:如果您的应用需要保存敏感数据,如登录凭据,可以考虑使用加密来保护这些数据。
     */
    private static class  EncryptionUtils {
        private static final String AES_ALGORITHM = "AES";
        private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding";
        private static final String SECRET_KEY = "your_secret_key_here"; // 需要更换为自己的密钥

        public static String encrypt(String data){
            try {
                SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), AES_ALGORITHM);
                Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
                cipher.init(Cipher.ENCRYPT_MODE, secretKey);
                byte[] encryptedData = cipher.doFinal(data.getBytes());
                return Base64.encodeToString(encryptedData, Base64.DEFAULT);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        public  static String decrypt(String encryptedData) {
            try {
                SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), AES_ALGORITHM);
                Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
                cipher.init(Cipher.DECRYPT_MODE, secretKey);
                byte[] decryptedData = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT));
                return new String(decryptedData);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
}

2. 使用数据库:

如果应用需要保存的用户信息较为复杂,可以考虑使用数据库来存储用户的登录状态。SQLite是一种轻量级的嵌入式数据库,它不需要服务器支持,直接在本地设备上运行。可以将用户的登录状态和会话信息保存在本地数据库中,用户切换角色后再次进入应用时,从数据库中读取这些信息来恢复登录状态。

package com.login;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class LoginDatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "login_status.db";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "login_status";
    private static final String COLUMN_ID = "id";
    private static final String COLUMN_IS_LOGGED_IN = "is_logged_in";

    private static final String SECRET_KEY = "your_secret_key_here";

    /**
     * 接收一个上下文对象,并调用父类的构造函数,指定数据库名称、版本号等参数,用于创建或打开数据库。
     * @param context
     */
    public LoginDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    /**
     * 在数据库第一次创建时调用,创建一个名为 login_status 的表,包含两个列:id 和 is_logged_in。
     * @param db The database.
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTableQuery = "CREATE TABLE " + TABLE_NAME + " (" +
                COLUMN_ID + " INTEGER PRIMARY KEY," +
                COLUMN_IS_LOGGED_IN + " TEXT)";
        db.execSQL(createTableQuery);
    }

    /**
     * 在数据库需要升级时调用,删除旧表并重新创建。
     * @param db The database.
     * @param oldVersion The old database version.
     * @param newVersion The new database version.
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 备份旧表数据
        String backupTableName = TABLE_NAME + "_backup";
        db.execSQL("ALTER TABLE " + TABLE_NAME + " RENAME TO " + backupTableName);
        // 创建新表
        onCreate(db);
        // 导入数据到新表
        String columns = COLUMN_ID + ", " + COLUMN_IS_LOGGED_IN;
        db.execSQL("INSERT INTO " + TABLE_NAME + " SELECT " + columns + " FROM " + backupTableName);
        // 删除备份表
        db.execSQL("DROP TABLE IF EXISTS " + backupTableName);
    }

    /**
     * 保存登录状态到数据库中。首先获取可写数据库实例,然后将登录状态加密后存储到 login_status 表中的 is_logged_in 列。
     * @param isLoggedIn
     */
    public void saveLoginStatus(boolean isLoggedIn) {
        try {
            SQLiteDatabase db = getWritableDatabase();
            ContentValues values = new ContentValues();
            values.put(COLUMN_IS_LOGGED_IN, encrypt(String.valueOf(isLoggedIn)));
            db.insert(TABLE_NAME, null, values);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 从数据库中读取登录状态。首先获取可读数据库实例,然后执行查询语句获取 is_logged_in 列的值。读取到的值是经过加密的,
     * 需要解密后返回给调用者。
     * @return
     */
    public boolean getLoginStatus() {
        Cursor cursor = null;
        try {
            SQLiteDatabase db = getReadableDatabase();
            cursor = db.rawQuery("SELECT * FROM " + TABLE_NAME, null);
            if (cursor != null && cursor.moveToFirst()) {
                String encryptedStatus = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_IS_LOGGED_IN));
                String decryptedStatus = decrypt(encryptedStatus);
                return Boolean.parseBoolean(decryptedStatus);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return false;
    }

    /**
     * 使用 AES 加密算法对数据进行加密。使用预设的密钥 SECRET_KEY,对给定的数据进行 AES 加密,并返回加密后的结果。
     * @param data
     * @return
     * @throws Exception
     */
    private String encrypt(String data) throws Exception {
        SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedData = cipher.doFinal(data.getBytes());
        return Base64.encodeToString(encryptedData, Base64.DEFAULT);
    }

    /**
     * 使用 AES 加密算法对数据进行解密。使用预设的密钥 SECRET_KEY,对给定的密文进行 AES 解密,并返回解密后的结果。
     * @param encryptedData
     * @return
     * @throws Exception
     */
    private String decrypt(String encryptedData) throws Exception {
        SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decryptedData = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT));
        return new String(decryptedData);
    }
}

3. 使用全局变量:

可以在单例类中定义全局变量来保存用户的登录状态。单例类的生命周期与应用程序运行时相同。通过在单例类中定义全局变量,可以灵活地访问这些变量,结合 EventBus 通信,从而实现用户登录状态的共享。

package com.login;

import org.greenrobot.eventbus.EventBus;

/**
 * 这段代码演示了如何使用全局变量 isLoggedIn 来保存和读取用户的登录状态。全局变量可以在应用的任何地方访问,但需要注意一些
 * 问题:
 * <p>
 *     线程安全性:全局变量在多线程环境下可能会存在竞态条件(race condition)的问题,需要确保对全局变量的访问是线程安全
 *     的。通过单例模式解决
 * </p>
 * <p>
 *     生命周期管理:全局变量的生命周期和应用的生命周期相同,如果应用被销毁,全局变量也会被销毁,需要谨慎管理。
 * </p>
 * <p>
 *     代码可维护性:过多使用全局变量会导致代码的可维护性下降,因为全局变量使得数据流动变得不可控,不利于代码的理解和调试。
 * </p>
 */
public class LoginManagerInstance {
    private static LoginManagerInstance instance;
    private boolean isLoggedIn = false;

    private LoginManagerInstance() {
        // 私有构造函数,避免外部实例化
    }

    public static synchronized LoginManagerInstance getInstance() {
        if (instance == null) {
            instance = new LoginManagerInstance();
        }
        return instance;
    }

    public void saveLoginStatus(boolean isLoggedIn) {
        this.isLoggedIn = isLoggedIn;
        // 发送登录状态变化事件
        EventBus.getDefault().post(new LoginStatusChangeEvent(isLoggedIn));
    }

    public boolean getLoginStatus() {
        return isLoggedIn;
    }

    public static class LoginStatusChangeEvent {
        private boolean isLoggedIn;

        public LoginStatusChangeEvent(boolean isLoggedIn) {
            this.isLoggedIn = isLoggedIn;
        }

        public boolean isLoggedIn() {
            return isLoggedIn;
        }
    }
}

订阅类这么处理登录

package com.login

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.login.LoginManagerInstance.LoginStatusChangeEvent
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe

/**
 * 登录界面
 */
class LoginActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 注册事件
        EventBus.getDefault().register(this)
    }

    public override fun onDestroy() {
        super.onDestroy()

        // 取消注册事件
        EventBus.getDefault().unregister(this)
    }

    // 订阅事件
    @Subscribe
    fun onLoginStatusChanged(event: LoginStatusChangeEvent) {
        val isLoggedIn = event.isLoggedIn
        // 处理登录状态变化事件
        handleLoginStateChanged()
    }

    private fun handleLoginStateChanged() {}
}

4. 使用Token:

在用户登录成功后,服务器通常会返回一个Token,这个Token可以用来验证用户的登录状态。可以将这个Token保存在本地,例如使用SharedPreferences或者数据库。当用户切换角色并重新进入应用时,可以通过携带这个Token向服务器请求数据,从而保持登录状态。

使用Token来保存用户的登录状态是一种常见的方法,适用于需要跨系统或跨应用共享登录状态的情况。

package com.login;

import android.content.Context;
import android.content.SharedPreferences;

/**
 * 当用户登录成功后,可以调用saveToken方法将Token保存到SharedPreferences中。当用户切换角色并重新进入应用时,可以调用
 * getToken方法从SharedPreferences中读取Token,并根据返回的值来恢复用户的登录状态。
 */
public class TokenManager {

    private static final String PREF_NAME = "login_status";
    private static final String KEY_TOKEN = "token";

    private Context context;

    public TokenManager(Context context) {
        this.context = context;
    }

    public void saveToken(String token) {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(KEY_TOKEN, EncryptionUtils.encrypt(token));
        editor.apply();
    }

    public String getToken() {
        SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        return EncryptionUtils.decrypt(sharedPreferences.getString(KEY_TOKEN, null));
    }
}

需要注意的是,在使用Token保存敏感信息时,应该对数据进行加密处理,以确保安全性。此外,还需要考虑到异常情况的处理,例如用户登出或者长时间未操作导致的自动退出登录。在设计时,应该结合具体的应用场景和用户需求,选择最合适的方法来实现保持登录状态的功能。

结合 okhttp 进行登录状态保存逻辑如下:


    public OkHttpClient getOkHttpClient() {
        return new OkHttpClient.Builder()
                .addInterceptor(new AuthInterceptor())
                .build();
    }

    private class AuthInterceptor implements Interceptor {
        @NotNull
        @Override
        public Response intercept(@NotNull Chain chain) throws IOException {
            Request request = chain.request();
            String token = getToken();
            if (token != null) {
                request = request.newBuilder()
                        .header("Authorization", "Bearer " + token)
                        .build();
            }
            return chain.proceed(request);
        }
    }

注意:这里使用了Bearer令牌(Bearer token)的形式来传递Token。Bearer令牌是OAuth 2.0中定义的一种访问令牌(Access Token)类型,用于在客户端和资源服务器之间进行身份验证。

Authorization: Bearer abc123

5. 单点登录(SSO)

单点登录(Single Sign-On,简称SSO)是一种用于实现用户在一个应用中登录后,无需再次登录其他应用的认证机制。它允许用户使用一个统一的账号和密码来访问多个应用,而无需为每个应用单独进行身份验证。

package com.login;

import android.content.Context;
import android.content.SharedPreferences;

import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.lang.ref.WeakReference;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * 要实现单点登录(SSO)机制,您需要一个中心化的认证服务器,用户在认证服务器上登录后,会生成一个令牌(Token),然后在访问
 * 其他应用时,将这个令牌发送给其他应用进行验证。以下是一个简单的示例,结合OkHttp实现SSO机制:
 *
 * <p>假设认证服务器的地址为 https://auth.example.com,提供了以下接口:</p>
 *
 * <li>
 * 1. /login:用户登录接口,需要提供用户名和密码,成功登录后返回Token。
 * </li>
 * <li>
 * 2. /validateToken:验证Token接口,用于验证Token的有效性。
 * </li>
 */
public class SsoTokenManager {

    private static final String PREF_NAME = "login_status";
    private static final String KEY_AUTH_TOKEN = "token";
    private static final String KEY_REFRESH_TOKEN = "token";

    private static final String AES_ALGORITHM = "AES";
    private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding";
    private static final String SECRET_KEY = "your_secret_key_here"; // 需要更换为自己的密钥
    private static final String AUTH_SERVER_URL = "https://auth.example.com"; // 认证服务器URL

    private WeakReference<Context> contextRef;
    private OkHttpClient httpClient;

    public SsoTokenManager(Context context) {
        this.contextRef = new WeakReference<>(context);
        this.httpClient = createHttpClient();
    }

    private OkHttpClient createHttpClient() {
        return new OkHttpClient.Builder()
                .addInterceptor(new AuthInterceptor())
                .build();
    }

    /**
     * 保存认证令牌和刷新令牌
     * @param authToken
     * @param refreshToken
     */
    public void saveToken(String authToken, String refreshToken) {
        try {
            String encodedAuthToken = EncryptionUtils.encrypt(authToken);
            String encodedRefreshToken = EncryptionUtils.encrypt(refreshToken);

            Context context = contextRef.get();
            if (contextRef != null) {
                SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString(KEY_AUTH_TOKEN, encodedAuthToken);
                editor.putString(KEY_REFRESH_TOKEN, encodedRefreshToken);
                editor.apply();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String getToken() {
        try {
            Context context = contextRef.get();

            if (context != null) {
                SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
                String encodedToken = sharedPreferences.getString(KEY_AUTH_TOKEN, null);
                if (encodedToken != null) {
                    return EncryptionUtils.decrypt(encodedToken);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 通常在发起需要验证登录状态的网络请求前调用。例如,在发起每个网络请求之前,您可以先调用 isTokenValid 方法来检查当
     * 前保存的 Token 是否有效,如果有效则继续发起网络请求,如果无效则需要重新登录获取新的 Token。
     * 举例:
     * public void makeAuthenticatedRequest() {
     * if (tokenManager.isTokenValid()) {
     * // Token 有效,可以发起网络请求
     * Request request = new Request.Builder()
     * .url("https://api.example.com/data")
     * .build();
     * tokenManager.getHttpClient().newCall(request).enqueue(new Callback() {
     *
     * @return
     * @Override public void onFailure(Call call, IOException e) {
     * e.printStackTrace();
     * }
     * @Override public void onResponse(Call call, Response response) throws IOException {
     * if (response.isSuccessful()) {
     * // 处理请求成功的情况
     * } else {
     * // 处理请求失败的情况
     * }
     * }
     * });
     * } else {
     * // Token 无效,需要重新登录
     * // 这里可以跳转到登录页面或者执行其他操作
     * }
     * }
     */
    public boolean isTokenValid() {
        String token = getToken();
        if (token == null) {
            return false;
        }
        // 向认证服务器发送请求验证Token是否有效
        Request request = new Request.Builder()
                .url(AUTH_SERVER_URL + "/validateToken")
                .header("Authorization", "Bearer " + token)
                .build();
        try (Response response = httpClient.newCall(request).execute()) {
            return response.isSuccessful();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    public void refreshAuthToken(String refreshToken) {
        // 使用刷新令牌获取新的认证令牌
        requestToken("username","password", refreshToken);
    }

    /**
     * 在需要用户登录的地方,调用 SsoTokenManager 的 requestToken 方法来获取Token,并保存到本地
     *
     * @param username
     * @param password
     */
    public void requestToken(String username, String password, String refreshToken) {
        // 构建请求体
        RequestBody requestBody = new FormBody.Builder()
                .add("username", username)
                .add("password", password)
                .build();

        // 构建请求
        Request request = new Request.Builder()
                .url(AUTH_SERVER_URL + "/login")
                .header("Authorization", "Bearer " + refreshToken)
                .post(requestBody)
                .build();

        // 发送请求
        httpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    String responseBody = response.body().string();
                    try {
                        JSONObject jsonObject = new JSONObject(responseBody);
                        String authToken = jsonObject.getString("auth_token");
                        String refreshToken = jsonObject.getString("refresh_token");
                        saveToken(authToken, refreshToken);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                } else {
                    // 处理登录失败的情况
                }
            }
        });
    }


    private class AuthInterceptor implements Interceptor {
        @NotNull
        @Override
        public Response intercept(@NotNull Chain chain) throws IOException {
            Request request = chain.request();
            String token = getToken();
            if (token != null) {
                request = request.newBuilder()
                        .header("Authorization", "Bearer " + token)
                        .build();
            }
            return chain.proceed(request);
        }
    }
}

需要注意的是,在使用单点登录时,需要确保服务器端支持相应的认证机制,并且需要对数据进行加密处理以确保安全性。这里考虑了异常情况的处理,例如用户登出或者长时间未操作导致的自动退出登录。

6. 使用AccountManager:

Android系统提供了一个AccountManager服务,可以用来管理用户的账户信息。可以通过创建一个新的账户或者使用已有的账户来保存用户的登录状态。这样即使用户切换角色,只要账户信息没有变化,就可以保持登录状态。

github.com/apachecn/ap…

7. 退出登录问题解决(超时自动退出等)

要解决用户登出或长时间未操作导致的自动退出登录问题,您可以考虑以下几种方法:

  1. 定时检查令牌有效性: 在用户登录后,定时检查认证令牌的有效性。如果发现令牌已失效,可以引导用户重新登录获取新的令牌。

  2. 使用刷新令牌(Refresh Token): 在用户登录后,除了认证令牌外,还生成一个刷新令牌。刷新令牌用于获取新的认证令牌,而无需用户重新输入用户名和密码。在认证令牌失效时,可以使用刷新令牌来获取新的认证令牌。

  3. 监控用户操作状态: 在用户登录后,监控用户的操作状态。如果发现用户长时间没有操作,可以视为用户已经退出登录,并清除认证令牌。

  4. 在用户主动退出登录时清除认证令牌: 在用户执行退出登录操作时,清除认证令牌,确保用户下次访问时需要重新登录。

  5. 使用单点登录(SSO)机制: 如果您的应用支持多个子应用,可以考虑使用单点登录(SSO)机制。用户在主应用登录后,其他子应用也可以共享该登录状态,无需用户重新登录。