思维导图

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)机制。用户在主运用登录后,其他子运用也能够同享该登录状况,无需用户从头登录。