资讯动态

Android保存QQ密码实战:SharedPreferences存储与AES加密详解

发布时间:2026/10/13 16:26:31 来源:尧图企业网站定制
简介面向初学者的Android Studio实战工程聚焦于保存与读取QQ密码这类登录凭证主要讲解SharedPreferences轻量级数据存储机制在真实场景中的用法适合刚接触Android数据持久化与界面事件处理的开发者。压缩包共941个文件大小6.31MB包含438张png图片、414个xml布局与配置、9个java源文件以及class、jar、apk等编译产物目录结构完整可导入Android Studio后直接查看运行。已有2263人学习下载。通过这份工程可以掌握SharedPreferences的读写调用、EditText与Button的界面绑定方式、简单的密码加密与解密思路并借助附带的app-debug.apk快速验证效果为后续实现自动登录等功能打下基础。1. 保存QQ密码的Android实战这个小工程为什么值得动手保存QQ密码乍看是个极小的工具需求但把它当做一个完整的Android Studio工程来做你会立刻发现自己被逼着处理三件事账号数据存在哪、应用被杀之后怎么读回来、以及密码会不会被别人轻易翻走。这三个问题对应着数据持久化、生命周期和加密设计也是之后做任何登录类App都绕不开的底层能力。这篇笔记面向那些能写Activity但还没独立做过存储层的开发者。我会从建工程开始把保存、读取、加密和常见坑全部铺开最后给出可以照着敲的Java源码与验证步骤。这套东西做完你对本地存储的边界会有一个很具体的认识。2. 工程落地与存储选型SharedPreferences为什么是保存QQ密码的首选2.1 存储方案对比SharedPreferences、SQLite与文件方案的取舍在Android上做本地保存绕不开三种路径SharedPreferences、SQLite、普通文件读写。绝大多数人第一次写保存功能时都分不清它们后面才发现选错方案的成本远高于多写几行代码的成本。先给结论保存QQ密码这种单条键值数据SharedPreferences最合适。SharedPreferences是系统提供的键值对存储数据以XML格式落在应用私有目录。它最适合存几十KB以内的轻量数据比如记住账号、应用设置、开关状态。它读写API足够简单两行代码就能写入不需要建表和写SQL而且系统自动处理了文件目录和加载时机你不需要关心文件系统层面的并发和缓冲。什么时候不该用它数据量到几十上百条后或者后续需要“查询所有账号中密码快过期的人”这种条件检索时SharedPreferences会非常吃力。SQLite才是正路。SQLite支持建表、索引、事务多账号密码本用它可以管理得很规矩代价是需要自己写SQL、写升级迁移新手容易在数据库版本升级上翻车。普通文件则适合图片、日志、导出文件这类非结构化内容用来存密码需要自己搞定序列化和并发写入属于没事找事。存储方案适合场景写入性能实现复杂度SharedPreferences少量键值对、记住账号、应用配置快apply异步低SQLite多条记录、条件查询、事务中高文件读写大文件、自定义格式、图片缓存取决于格式中2.2 新建Android Studio工程Gradle配置与项目结构用Android Studio新建一个空Activity项目IDE已经把Gradle配置和目录骨架生成好了。你只需要关注三个核心配置项。minSdk决定最低支持到哪个Android版本我习惯设21Android 5.0覆盖存量设备的同时不需要处理古董版本兼容。targetSdk代表针对哪个系统版本做了适配新版系统对明文流量、分区存储等有更严的约束targetSdk越高你的代码越早接触这些约束这是好事。plugins { id com.android.application } android { namespace com.example.qqpasswordstore // R类与BuildConfig的包名 compileSdk 34 defaultConfig { applicationId com.example.qqpasswordstore // 安装到设备上的唯一标识 minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.constraintlayout:constraintlayout:2.1.4 }这里重点解释三个容易混淆的点。第一namespace和applicationId可以不一样但为了少踩坑最好保持一致否则R类路径用的是namespace第三方SDK拿applicationId标识应用两边不一致有时会出妖蛾子。第二compileSdk 34表示用较新的API来编译你的测试设备版本低于34也能编译compileSdk跟运行版本没有直接关系。第三appcompat必须保留因为MainActivity继承的AppCompatActivity在androidx.appcompat包里去掉它Activity会报ClassNotFoundException。目录上只需要记住几个位置Java源码在app/src/main/java/包名路径布局在app/src/main/res/layout/应用图标在mipmap系列文案在values/strings.xml。刚入门的人经常在Android视图下找不到文件在磁盘哪切到Project视图就能看到完整物理路径。2.3 登录界面布局控件id命名约定与密码输入属性界面做成最常用的两行输入加复选框加按钮。账号输入框在上面密码输入框在下面中间一个“记住密码”的CheckBox底部一个“保存”按钮。整个布局用LinearLayout垂直排列嵌套少渲染快。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding24dp TextView android:layout_widthwrap_content android:layout_heightwrap_content android:textQQ账号 / EditText android:idid/et_qq_account android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入QQ账号 android:inputTypetext / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text密码 / EditText android:idid/et_qq_password android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入密码 android:inputTypetextPassword / CheckBox android:idid/cb_remember_password android:layout_widthwrap_content android:layout_heightwrap_content android:text记住密码 / Button android:idid/btn_save android:layout_widthmatch_parent android:layout_heightwrap_content android:text保存 / /LinearLayoutid命名我坚持一套约定控件缩写加下划线加业务名。et_开头的EditTextcb_开头的CheckBoxbtn_开头的Button。没有这套约定Activity里同时有账号、密码、邮箱三个输入框时findViewById那一段代码会混乱到无法阅读。再提两个属性inputTypetextPassword会把输入内容显示成圆点这是密码框的标配android:hint是输入框未输入时显示的灰色提示文字既能让用户明白该填什么又不会占用正文空间。工程骨架搭好了存储方案定了下面进入实际的保存逻辑。3. 核心实现把QQ密码写进SharedPreferences的完整代码3.1 封装PasswordStore工具类读写集中在一处很多教程代码把SharedPreferences直接写在Activity里一个方法里读一个方法里写看着能跑后面加个加密换存储所有调用点都要跟着改。我习惯的做法是先把数据访问封装成一个PasswordStore类Activity只负责调save和get至于数据写到哪个文件、是明文还是密文Activity完全不关心。public class PasswordStore { private static final String PREF_NAME qq_password_store; private static final String KEY_ACCOUNT account; private static final String KEY_PASSWORD password; private final SharedPreferences prefs; public PasswordStore(Context context) { // MODE_PRIVATE仅当前应用可读写该文件 prefs context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); } public void save(String account, String password) { prefs.edit() .putString(KEY_ACCOUNT, account) .putString(KEY_PASSWORD, password) .apply(); } public String getAccount() { return prefs.getString(KEY_ACCOUNT, ); } public String getPassword() { return prefs.getString(KEY_PASSWORD, ); } public void clear() { prefs.edit().clear().apply(); } }构造函数里的Context是所有系统服务访问的入口getSharedPreferences的第一个参数是文件名第二个参数是文件权限位。MODE_PRIVATE表示只有本应用能读写这是Android默认也最常用的文件模式。save方法里prefs.edit()返回一个Editor对象两个putString分别写入账号与密码最后由apply()一次提交。这里两个putString合在一次提交里比分两次edit().putString再apply()少一次磁盘回写。getString的第二个参数是默认值文件里没有对应键时返回空字符串而不是null。这个细节一开始不值钱一旦你在Activity里写if (password ! null)这种判断就会陷入一堆空判断里。我的习惯是所有get方法都返回合理的默认值调用方只需要判断isEmpty代码会干净很多。3.2 Activity里把按钮与CheckBox串起来工具类写好了把它接进MainActivity。MainActivity干三件事findViewById拿控件引用、给保存按钮设置点击监听、在监听里判断CheckBox是否勾选再决定是否保存。public class MainActivity extends AppCompatActivity { private EditText etAccount; private EditText etPassword; private CheckBox cbRemember; private PasswordStore store; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); etAccount findViewById(R.id.et_qq_account); etPassword findViewById(R.id.et_qq_password); cbRemember findViewById(R.id.cb_remember_password); store new PasswordStore(this); findViewById(R.id.btn_save).setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { String account etAccount.getText().toString().trim(); String password etPassword.getText().toString(); if (account.isEmpty() || password.isEmpty()) { Toast.makeText(MainActivity.this, 账号和密码不能为空, Toast.LENGTH_SHORT).show(); return; } if (cbRemember.isChecked()) { store.save(account, password); Toast.makeText(MainActivity.this, 已记住密码, Toast.LENGTH_SHORT).show(); } else { store.save(, ); Toast.makeText(MainActivity.this, 未勾选记住不保存, Toast.LENGTH_SHORT).show(); } } }); } }两个细节值得单独讲。第一个是account用trim()去掉首尾空格防止复制账号时带进奇怪的空格导致登录失败password不能trim因为密码本身可以以空格开头或结尾去掉就破坏了密码原文。第二个是没勾选“记住密码”时我调用store.save(, )而不是什么都不做。为什么用户上一轮可能勾选过并保存了密码这次取消勾选意味着他想把历史记录清掉。主动写空串等于覆盖旧数据如果什么都不做旧密码会一直躺在文件里。3.3 apply与commit保存密码该选哪个SharedPreferences提交有apply和commit两个方法很多人随手用一个没想过区别。commit是同步写盘调用后当前线程阻塞直到数据写完返回boolean告诉你是否成功apply是异步写盘先立刻更新内存里的数据再排队往磁盘写方法本身没有返回值。按这个区别去选如果保存密码后你的App立刻要执行“杀掉进程”或“退出登录并依赖已保存结果”这样的操作commit更稳妥。但日常点保存按钮后就继续操作界面apply完全够用而且不会卡住UI。我在线上见过一个日志应用主线程里每次写配置都调commit数据量一大保存时界面明显一顿一顿的。PasswordStore的save里我用的是apply想换成commit只需要替换提交那一行API签名一致。4. 读取回填与加密让保存的密码在下次打开时直接可用4.1 启动自动填充onCreate里读回账号密码并回填能存进去还得能读回来。在MainActivity的onCreate里setContentView与findViewById之后追加上回填逻辑让用户下次打开App时直接看到上次保存的账号密码。// 在 onCreate 中 findViewById 和 PasswordStore 初始化之后添加 String savedAccount store.getAccount(); if (!savedAccount.isEmpty()) { etAccount.setText(savedAccount); etPassword.setText(store.getPassword()); cbRemember.setChecked(true); }这段逻辑用savedAccount的isEmpty做判断因为账号和密码是成对保存的账号没保存则密码一定也没有。setText将保存的字符串填充进EditTextsetChecked(true)把复选框的状态也恢复成勾选让界面与本地数据一致。注意回填只应写在onCreate里不要写进onResume。背景切换时onResume也会执行如果用户在编辑界面时切到后台再回来onResume里的setText会把用户刚输入但还没保存的内容覆盖回旧值这就是典型的生命周期使用不当。4.2 给密码加一层AES加密代码与参数边界SharedPreferences本身不加密生成的XML文件在/data/data/包名/shared_prefs/目录下。普通App访问不了别的应用目录但root过的设备、调试时的adb run-as命令都可以直接读文件内容。只教存不教加密等于给用户埋雷。我一般只对密码字段加密账号保持明文便于日后的账号检索。public class CryptoUtil { // 硬编码密钥仅用于教学演示真实工程要用 Android Keystore 管理密钥 private static final String SECRET_KEY 0123456789abcdef; private static final String ALGORITHM AES; public static String encrypt(String content) { try { SecretKeySpec keySpec new SecretKeySpec( SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM); Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] bytes cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); return Base64.encodeToString(bytes, Base64.NO_WRAP); } catch (Exception e) { return content; } } public static String decrypt(String content) { try { SecretKeySpec keySpec new SecretKeySpec( SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM); Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] bytes cipher.doFinal(Base64.decode(content, Base64.NO_WRAP)); return new String(bytes, StandardCharsets.UTF_8); } catch (Exception e) { return content; } } }几个参数说明一下。SECRET_KEY是16字节的AES-128密钥长度必须是16字节多一个少一个都初始化失败。ECB模式是最基础的AES分组模式同样的明文产生同样的密文安全性不如CBC/GCM但代码最简单适合教学。PKCS5Padding是填充方案让任意长度的输入都能对齐16字节分组。Base64.NO_WRAP的细心点在于默认的Base64输出会带换行符换行符一旦进到XML里读取时密文串里就多了不可见字符AES解密必然失败。接入PasswordStore很简单save里把password字段改成CryptoUtil.encrypt(password)getPassword里先取出密文再包一层decrypt。注意一定要在保存和读取两侧同时改只改保存不改读取下次打开就看到一堆解密失败的乱码。4.3 多账号场景一个键放JSON还是换SQLite只存一个账号的demo可以练手但很多人的真实需求是管理多个QQ号。SharedPreferences一个键对应一个值天然不适合多账号。我的做法是把多套账号序列化成JSON数组整体存进一个键。Android自带org.json包不需要引第三方库。public void saveAll(ListQQAccount accounts) { JSONArray array new JSONArray(); try { for (QQAccount acct : accounts) { JSONObject obj new JSONObject(); obj.put(account, acct.account); obj.put(password, CryptoUtil.encrypt(acct.password)); array.put(obj); } prefs.edit().putString(accounts, array.toString()).apply(); } catch (JSONException e) { // 序列化失败时保留原数据避免覆盖已有记录 Log.e(PasswordStore, saveAll failed, e); } }JSONArray.toString()会把整个列表压缩成一行JSON字符串好处是读写仍然走同一个键不需要改表结构新增一个账号也只是重建JSON数组。坏处也很明显每改一个账号就要整份重写数据量到几百条后性能会退化。如果做的是严肃多账号密码管理器建议直接迁到SQLite建一张account表字段至少包含id、account、password、updated_at。JSON方案适合工具类App和教学工程SQLite适合产品化这个边界要记清楚。5. 密码保存避坑手册最容易翻车的5个现场5.1 保存成功重启App却读不到数据现象点保存时Toast提示“已记住密码”杀掉App进程重新打开输入框还是空的。很多人第一反应是apply没写完改成commit后问题依旧。原因数据没丢丢的是读写文件的名称不一致。最常见的组合是读取时用getPreferences(MODE_PRIVATE)而写入时用getSharedPreferences(qq_password_store, MODE_PRIVATE)。getPreferences不传文件名Android会自动拿当前Activity的类名当文件名也就是MainActivity.xmlgetSharedPreferences则使用你指定的qq_password_store.xml。保存写进A文件读取从B文件读自然取不到。解决整个App统一使用PREF_NAME并且保证所有读写都经过PasswordStore类不让别的类直接碰这个文件。验证方法保存后执行adb run-as 包名 cat shared_prefs/qq_password_store.xml看看文件路径和文件名是否与读取时一致也可以加日志把SharedPreferences底层对应的File路径打出来直接对比两个文件路径是否相同。5.2 中文密码乱码或解密直接抛异常现象密码里有中文或emoji时保存后再读出来不是原文加密场景下一直解密失败。原因getBytes()没带charset参数时使用系统默认编码。多数Android设备默认UTF-8但部分定制ROM或旧版本默认编码不同。AES加密与解密依赖同样的字节序列两边编码不一致出来的结果就对不上。解决全链路用StandardCharsets.UTF_8getBytes和new String两个方向都写死。同时检查Base64.NO_WRAP是否用了若密文里混入换行或空格解密时doFinal会因数据长度不符而直接抛异常。5.3 明文密码藏在xml里被调试工具翻出来现象Android Studio的Device Explorer或adb run-as命令直接打开shared_prefs目录下的xml文件明文账号密码一览无余。原因MODE_PRIVATE只是Linux文件权限的读写限制挡住的是其他普通应用挡不住调试环境、root设备和备份工具。数据一旦以明文落盘私密性就掌握在终端设备的使用者手上。解决第一层用AES加密密码字段再putString第二层在AndroidManifest.xml的application节点上设android:allowBackupfalse避免密码被系统云备份迁移到其他设备第三层发布release包时保持debuggable为false别把测试期开着debug的配置带上线。5.4 新旧版本数据格式冲突加密上线后旧数据全读不出现象某版本上线后老用户反馈所有已保存的账号都读不出密码。检查发现旧版本存的是明文新版本读取时直接调decrypt方法AES解密旧明文必然失败。原因同一个KEY_PASSWORD在旧版存明文、新版存密文代码却只用新逻辑去读。版本迭代时存储格式升级这是最容易被忽略的兼容性问题。解决读取时做版本兼容判断。常见做法是写入密文时加统一前缀比如ENC:读取时判断该前缀是否存在存在才走decrypt否则按旧逻辑返回原串另一个做法是换一个全新的键名存新格式数据让新旧数据彻底隔离。不管选哪种升级存储格式时都要保留旧格式的读取路径这是存储层开发的基本原则。5.5 release包被反编译硬编码的加密密钥直接暴露现象打好的release包被别人反编译DEX文件里明晃晃躺着CryptoUtil里那个SECRET_KEY字符串加密形同虚设。原因Java字符串常量编译后会直接出现在DEX字节码里反编译工具能把它读出来。密钥一旦被人拿到所有本地密文都能解回明文。解决教学项目可以硬编码密钥但发布产品不要这么做。Android Keystore是系统提供的安全隔离区域密钥生成后私钥不可导出应用只能调用它的加密解密接口密钥本身不会出现在DEX里。如果暂时不上Keystore至少要把密钥拆成多段字符串在运行时拼接提高从反编译里直接定位的难度但这只是权宜之计。6. 从Demo到能用的密码小工具验证清单与两个进阶习惯6.1 验证清单模拟真实场景的测试步骤功能做完不要急着收工按下面这张清单过一遍很多隐藏问题都是在测试阶段暴露的。测试场景操作步骤预期结果保存勾选记住密码点击保存日志中能看到SharedPreferences写入xml里是密文而非明文回填杀掉进程重新打开App账号密码自动填充复选框保持勾选不保存取消勾选点保存本地旧数据被清空重启后输入框为空中文密码保存含中文和emoji的密码重启后能正确解密还原空输入账号为空直接保存Toast提示“账号和密码不能为空”不执行写入数据备份检查manifest里的allowBackup密码类应用建议设置为false这六项里中文密码和杀掉进程重开几乎每次改完存储层都能抓出问题建议形成肌肉记忆每次提交前都手动跑一遍。6.2 两个进阶习惯Keystore升级路径与密码不入库进阶一是把CryptoUtil换成Android Keystore方案。做法是在Keystore里生成非对称密钥对用公钥加密AES密钥再把AES密钥存到SharedPreferences使用时从Keystore取私钥解密出AES密钥再用AES密钥解密码字段。这样真正敏感的私钥永远不出安全硬件应用被杀重启后依然可以解密。这个升级只需要改PasswordStore和CryptoUtil内部实现Activity不需要动封装的价值在这里体现得最明显。进阶二是改变思考方式真正严肃的密码管理App不会把用户密码作为可逆数据直接保存而是存密码的哈希或口令派生值配合生物识别解锁。这个阶段不做不代表不该懂至少要知道SharedPreferences加密只是起点产品化路上的密码存储比Demo里复杂得多。我自己的习惯是把存储层彻底隔离出来独立成类禁止Activity直接访问SharedPreferences。曾在图上省事直接把保存逻辑写在Activity里后来加加密和多账号时改到怀疑人生。所以无论你的工程现在有多小都值得把存储独立成类这个习惯会在项目的下一个版本里救你。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑