☰
Android记住密码功能实战:SharedPreferences存储与加密改造
2026/10/13 19:41:29 网站建设 项目流程

简介:面向安卓初学者的实战工程,基于Android Studio工具编写,演示如何通过SharedPreferences保存QQ密码。工程覆盖数据存储、界面布局、按钮事件与简单加密解密逻辑,包含保存和加载两个核心功能,通过Editor写入键值对,并对密码进行加密后再存储,帮助理解移动应用本地持久化的常用方式。同时,工程完整呈现了从输入框获取密码、触发按钮点击、更新界面回显的交互闭环,适合作为Android入门练手。资源共941个文件,以414个XML布局与配置文件、438张PNG图片为主,另含Java源码、可直接安装的APK、Gradle构建脚本及少量class、jar等编译产物,压缩包大小约6.31MB。其中XML负责界面与资源定义,PNG提供视觉素材,APK支持真机安装验证,各类型文件协同构成完整可运行工程。已有2263人学习下载,可用于课程设计、自学入门或项目模板参考。通过完整源码与配套界面,能够掌握SharedPreferences读写流程、UI布局与事件监听的配合方法,同时建立登录信息加密保存的安全意识,为后续学习Android Keystore等更强方案奠定基础。

1. 先认清这份“保存QQ密码”工程到底在解决什么问题

拿到一份命名为“保存QQ密码”的Android Studio工程,第一反应容易跑偏:是保存别人的密码,还是保存自己App里的登录密码?拆开看之后可以确定,这是一份标准的登录模块实战——仿QQ登录界面的“记住密码”功能,把用户勾选“记住密码”后的账号和密码写到本地,下次启动自动回填。它解决的是Android开发里三个高频问题:登录态怎么持久化、密码存哪里才不至于被一览无余、界面怎么恢复上次的输入状态。适合刚能跑通Hello World、想拿完整工程练手的新手,也适合要把登录模块复用到聊天App或毕设项目里的同学。我会照着这份源码把存储选型、读写封装、加密改造和几个翻车点挨个拆一遍。

2. 存储方案选型:密码该进SharedPreferences还是SQLite

2.1 Android本地存储四兄弟:定位和边界

做这个Demo之前,先得把Android给的本地存储路子盘清楚。我按使用频率排了个对比表,后面选型全看它:

存储方式本质适合场景典型坑
SharedPreferencesXML键值对小配置、登录态、开关项整个文件读入内存,不适合大字段
内部文件File私有目录文件图片、日志、大文本要自己管流、管编码
SQLite关系型数据库多表关联、复杂查询、大量记录版本升级要写迁移,容易崩
RoomSQLite的注解封装需要响应式查询的正式项目编译期生成代码,KSP配置麻烦

SharedPreferences最朴素,但它足够应付“账号+密码+勾选状态”这种两三个字段的小数据。很多人一上来就说“密码必须进SQLite”,其实单条键值对这种场景上SQLite是杀鸡用牛刀——你要多写好几十行数据库Helper,还要处理版本号和Cursor的关闭。我一般不会为一个记住密码功能去引一个数据库框架。

内部文件File在这里更不合适。它适合存图片或日志这类二进制或大文本,你要自己拼接、自己解析,存一个结构化的登录配置反而不方便。Room是SQLite的封装,适合那种需要按条件查询、列表展示数据的页面,比如聊天记录、订单列表。登录配置是“读取整份覆盖整份”的访问模式,正好落在SharedPreferences的甜区。

2.2 为什么这份工程选SharedPreferences当主存储

从源码的注释和代码习惯能看出,原作者选型时主要考虑了四点。

第一是数据量。这份工程需要持久化的只有账号字符串、密码字符串、布尔值的勾选状态,总共三个字段。SharedPreferences底层是一个XML文件,三个节点的文件打开和解析成本可以忽略。第二是访问模式。记住密码功能只需要“登录时写入、启动时读取、退出时清空”这三种整体操作,没有“查一条”“按条件统计”这种需求,键值对天然匹配。第三是生命周期。SQLite要求你管理数据库连接、事务和游标,SharedPreferences连关闭都不用管,系统加载完文件就放内存里了。第四是代码量。用SQLite实现同样的效果,差不多要多写两倍样板代码,对教学工程不友好。

什么时候该换方案?我给自己定的标准是:字段超过五个、需要按时间排序、需要模糊搜索,这三条命中任何一条,就别硬撑SharedPreferences了,直接上SQLite或Room。单纯保存登录信息,SharedPreferences就是最优解,不需要为了显得专业去过度设计。

2.3 源码结构:数据从按钮到XML的完整路径

拿到这份工程,打开包结构你会看到主要东西都集中在四个文件里:

  • MainActivity:登录页,包含账号输入框、密码输入框、记住密码勾选框和登录按钮。
  • HomeActivity:登录成功后的落地页,用来验证登录流程是否走通。
  • UserInfo:实体类,承载账号、密码、记住状态三个属性。
  • SpUtils:SharedPreferences的封装工具类,所有读写都走这里。

整个流程是:MainActivity里点击登录按钮,做非空校验,然后判断勾选框状态,把三个字段交给SpUtils写入到shared_prefs目录下的XML文件;下一次进入应用,MainActivity的onCreate里读取同一份XML,把账号回填到输入框,密码回填到密码框,勾选框状态也恢复。

这点要特别说明一下:这份工程叫“保存QQ密码”,但保存的是自己应用里用户输入的登录密码,不是去解析聊天软件的数据。真实App里,密码原文根本不应该在本地落盘——登录成功后服务端会下发一个token,客户端保存的应该是token,不是密码。这个工程的价值在于演示“登录持久化”这套完整链路,把链路里的每个环节跑通,后面接真实接口时把保存内容从密码换成token就行。

3. 把核心代码拆开:写入、读取、清空与界面联动

3.1 封装工具类:SpUtils的读写与清空

源码里最值得先看的是SpUtils。它把SharedPreferences的细节全部包起来,Activity里调一个方法就拿结果,不用关心键名和上下文。核心代码是这样的:

public class SpUtils { private static final String SP_NAME = "login_config"; private static final String KEY_ACCOUNT = "account"; private static final String KEY_PASSWORD = "password"; private static final String KEY_REMEMBER = "remember"; public static void saveLoginInfo(Context context, String account, String password, boolean remember) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString(KEY_ACCOUNT, account); editor.putString(KEY_PASSWORD, remember ? password : ""); editor.putBoolean(KEY_REMEMBER, remember); editor.apply(); } public static String[] getLoginInfo(Context context) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); String account = sp.getString(KEY_ACCOUNT, ""); String password = sp.getString(KEY_PASSWORD, ""); boolean remember = sp.getBoolean(KEY_REMEMBER, false); return new String[]{account, password, String.valueOf(remember)}; } public static void clearLoginInfo(Context context) { SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); sp.edit().clear().apply(); } }

这里有两个容易被忽略的动作。一个是写入时用了三目运算符:remember为false时,不写空字符串而是写入空串,把密码字段清掉。这样即使账号还在,下次启动也不会把密码回填出来。另一个是读取返回值用String数组,下标0是账号、1是密码、2是记住状态。这种方式简单但不够优雅,读代码的人容易忘记下标含义,我一般建议改成返回UserInfo实体类。

参数上要注意:SP_NAME是文件名,同一个应用里所有通过getSharedPreferences读取的地方必须用同一个名字,拼错一个字母就是两个不同文件,读出来永远是空。MODE_PRIVATE表示仅本应用可访问,这是目前唯一推荐的模式。MODE_WORLD_READABLE这类直接别用,从Android 7.0开始它们已经失效,编译期也会被标记警告。

3.2 登录按钮的完整逻辑与校验顺序

MainActivity里登录按钮的点击逻辑,源码里是这样组织的:

public void onLogin(View view) { String account = etAccount.getText().toString().trim(); String password = etPassword.getText().toString().trim(); if (account.isEmpty() || password.isEmpty()) { Toast.makeText(this, "账号和密码不能为空", Toast.LENGTH_SHORT).show(); return; } boolean remember = cbRemember.isChecked(); SpUtils.saveLoginInfo(this, account, password, remember); Intent intent = new Intent(this, HomeActivity.class); startActivity(intent); finish(); }

逻辑顺序是:先取文本、去空格,再做非空校验,处理完勾选状态后保存数据,最后跳转。这里的校验只是“不为空”的弱校验,真实项目一般会在Activity里简单判一下,再去请求接口做账号密码校验,成功后才走保存和跳转。

把保存写在跳转之前是有讲究的。如果先跳转再保存,页面切换的间隙里如果进程被杀,数据可能丢;先保存再做页面跳转,保证数据落盘动作已经发生。finish()把登录页关掉,避免用户按返回键时退回登录页,这样“下一次启动才需要重新登录”的语义才完整。

3.3 界面恢复:下次启动自动回填

数据存进去了,下一次启动怎么拿出来?MainActivity的onCreate里有一段读取逻辑:

String[] info = SpUtils.getLoginInfo(this); if (info != null && !TextUtils.isEmpty(info[0])) { etAccount.setText(info[0]); etPassword.setText(info[1]); cbRemember.setChecked(Boolean.parseBoolean(info[2])); etPassword.setSelection(info[1].length()); }

TextUtils.isEmpty的判断很重要:它同时拦住了空串和null,避免setText收到null导致崩溃。setSelection把光标移到密码末尾,这样用户如果想修改密码,直接接着输入而不是从头开始。

这里有个小坑在Boolean.parseBoolean上。它只有在字符串是“true”时才返回true,其他任何情况都返回false,包括传入null也不会抛异常。这个特性用对了是省事,用错了就会出现“我记得勾了记住密码,结果重启后没勾上”的玄学问题——检查一下你就发现是signup时存的布尔字段没对齐。

提示:onCreate里做回填时,一定要等setContentView执行完再拿控件引用,否则拿到的是null,调用setText会直接空指针。

三方库的写法到这里就结束了,这套原生代码没有引入任何额外依赖,对学习SharedPreferences来说反而是好事。

4. 别把密码当明文躺平:从MODE_PRIVATE到加密存储

4.1 明文存储的风险与真实场景边界

SharedPreferences存进去的XML文件长什么样?打开/data/data/应用包名/shared_prefs/login_config.xml,你会看到类似这样的内容:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="account">test_user</string> <string name="password">12345678</string> <boolean name="remember" value="true" /> </map>

密码就是明晃晃的12345678。日常使用中普通用户看不到这个文件,因为它在应用的私有目录里,需要root权限才能访问。但换个角度想:如果用户手机丢了、设备被root过、或者做了系统备份,这份XML就会暴露。很多同学在毕设里直接把密码明文存进去,答辩时被老师问一句“安全性体现在哪”就卡住了。

所以真实App里,登录密码的落盘规则一般是这样的:本地永不保存密码原文,服务端登录成功后签发一个token,客户端把这个token存到私有存储里。密码只在登录请求发出的那一瞬间存在于内存中,用完即焚。这份工程把密码存本地是教学演示,你可以保留这个逻辑,但要明白它是教学简化,不是生产实践的模板。

4.2 加密改造:用EncryptedSharedPreferences替换原文件

安全加密的落地方案,我推荐直接用Android官方提供的Jetpack Security组件。它基于Android Keystore系统生成密钥,加密后的文件不再是明文XML,即使被拖走,也只是一堆密文。

先把依赖加上:

implementation "androidx.security:security-crypto:1.1.0-alpha06"

然后在SpUtils里增加一个加密实例的初始化方法:

public static SharedPreferences getEncryptedPrefs(Context context) throws GeneralSecurityException, IOException { MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); return EncryptedSharedPreferences.create( context, "sec_login_config", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); }

这里几个参数要解释清楚。MasterKey是主密钥,由Android Keystore生成和管理,密钥本身不出安全硬件。AES256_GCM是密钥的加密方案,GCM模式带认证,可以防止密文被篡改。文件名换成了sec_login_config,和原来的明文XML分开存放。AES256_SIV针对key名称加密,保证别人看不出你存了哪些字段;AES256_GCM针对value加密,保护实际数据内容。

改造后的写入代码和原来一模一样,只是把getSharedPreferences换成getEncryptedPrefs。对外暴露的接口不变,调用方不用改一行代码。这是典型的“最小侵入改造”,工程里的SpUtils只需要替换内部实现。

4.3 兼容性与降级边界

用了加密库之后要注意一点:security-crypto要求minSdk 23(Android 6.0)。低于这个版本,MasterKey依赖的Keystore能力不够,库会直接抛异常。如果项目还要支持Android 5.0,就不能全量启用加密实例。做法是先判断SDK版本,高版本走加密分支,低版本退回原来的明文SharedPreferences,等用户升级系统后再慢慢淘汰。

验证加密是否生效,可以清掉应用数据重来一遍,然后再次打开那个XML文件,看到的应该是齐刷刷的Base64密文,而不是原来的明文密码。

5. 避坑指南:五个我实际翻过车的点

5.1 commit和apply差一个生命周期

现象:登录后保存密码,然后立刻用返回键退出应用,再启动时密码没被回填。在低版本机器上多按几次会出现。

原因:apply()是异步写盘,它先把数据更新到内存里的SharedPreferences副本,随后才在后台线程里写磁盘文件。进程在磁盘写入完成前被杀,这次写入就静默丢了。commit()是同步写盘,阻塞到写入完成才返回。

解决:登录保存这种低频写操作,直接用commit()。高频的开关、进度条这种可以继续apply(),性能差异只在频繁调用时才明显。我现在的习惯是:登录信息这种“丢了要命”的数据一律commit,其余用apply。

5.2 getPreferences和getSharedPreferences用混

现象:保存成功,换一个界面去读,读出来的全是默认值。代码检查了好几遍,键名没错、类型没错,就是读不到。

原因:getSharedPreferences(SP_NAME, MODE_PRIVATE)打开的是自定义文件名的XML;而getPreferences(MODE_PRIVATE)是Activity内部快捷键,它的文件名是当前Activity的类名。两个文件不同,读写自然对不上。

解决:全项目统一走SpUtils这个工具类,不要在Activity里直接调用getPreferences。工具类里把文件名写死,避免每个人各写一份。

5.3 在适配器或工具类里拿不到Context

现象:在一个静态工具方法里调用SpUtils.saveLoginInfo,IDE直接提示无法解析this。把Activity的this传进去又担心内存泄漏。

原因:SharedPreferences是Context提供的服务,没有Context就无从谈起。静态方法里经常把Context弄丢,这是封装时最常犯的问题。

解决:统一在Application初始化时把全局Context缓存下来。做法是写一个App类继承Application,在onCreate里保存this,然后SpUtils内部通过AppUtil.getContext()获取。这样任何地方都能调,又不需要一层层传Context。

5.4 密码框回填后明文显示

现象:保存密码后重启应用,密码被回填到输入框里,但显示的是明文字符,旁边还默认带了一个可见性开关。

原因:EditText没有设置TYPE_TEXT_VARIATION_PASSWORD属性,系统不知道这是密码框,按普通文本处理。

解决:布局文件里给密码框加inputType,代码设置的话是:

etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);

这样输入和回填都会显示圆点。但要注意,如果同时期望“密码可见”切换功能,还要自己处理InputType被覆盖的问题,用setTransformationMethod会更稳。

5.5 用户点了系统“清除数据”后一切归零

现象:测试反馈“我明明勾了记住密码,重启后又要重新登录”。查了一圈,代码没问题,最后发现对方是去系统设置里点了清除数据。

原因:SharedPreferences、数据库、内部文件都存放在应用私有目录,清除数据就是把整个目录删掉,这是系统行为,不是代码缺陷。

解决:告诉用户这是预期结果。如果产品要求“清除应用数据后还能恢复登录”,那这些数据就不能只存本地,要么走服务端会话恢复,要么使用Android的AccountManager做系统级账号同步。本地存储的定位永远是“便捷”,不是“可靠”。

6. 进阶技巧:给本地登录态加上保鲜期与验证习惯

6.1 加时间戳:让密码不是永久有效

现在这份工程的问题是:只要勾了记住密码,密码就永远躺在本地,永不失效。正式项目里一般会要求登录态过期,比如七天内免登录,超过七天必须重新输密码。实现方式不复杂,在保存时多写一个时间戳:

editor.putLong(KEY_LOGIN_TIME, System.currentTimeMillis());

读取时判断:

long savedTime = sp.getLong(KEY_LOGIN_TIME, 0L); if (System.currentTimeMillis() - savedTime > 7 * 24 * 60 * 60 * 1000L) { SpUtils.clearLoginInfo(context); // 跳回登录页 }

过期后清空本地信息,用户下次启动就是全新登录流程。这个时间戳只解决“过期”这一个问题,如果有人手动修改系统时间,它也会跟着失真,正式项目建议用服务端下发的时间或token过期时间来判断。

6.2 养成固定验证习惯

每次改完登录模块,我都会在真机上强制走完这三个场景:登录后杀进程重进,验证回填是否成功;把系统时间往后调几天再进应用,验证过期逻辑是否触发;去设置里清除应用数据,验证是否需要重新登录。这三个动作能覆盖掉前五章说的大多数问题。

这套“保存QQ密码”工程做教学素材是够格的,关键是把默认的明文保存改成加密保存,再把token概念代入理解一次,你的登录模块就和其他人的不在一个水平线了。这个工程里没有用任何奇怪框架,全部是原生API,适合作为你改造自定义登录模块的底稿。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询