1、演示视频
基于Java Swing的本地密码管理器
2、项目截图
设计说明
3.1 整体架构设计
项目采用分层设计思想,分为界面层、业务逻辑层、数据存储层、加密算法层,各层职责清晰,低耦合高内聚:
- 界面层(GUI):基于Swing框架实现,包含主界面、添加密码对话框、修改密码对话框,负责用户交互;
- 业务逻辑层:包含密码生成、数据校验、事件处理等逻辑,衔接界面层和数据存储层;
- 数据存储层:负责本地JSON文件的读写,实现密码条目的增删改查;
- 加密算法层:封装AES加密解密逻辑,为密码存储提供安全保障。
3.2 类结构设计
| 类名 | 所属层级 | 核心职责 |
|---|---|---|
| AESUtil | 加密算法层 | 实现AES-128加密/解密,提供加密解密静态方法 |
| PasswordEntry | 数据模型层 | 密码条目实体类,封装网站、账号、加密密码属性 |
| PasswordStorage | 数据存储层 | 处理本地JSON文件读写,实现密码条目的增删改查 |
| PasswordGenerator | 业务逻辑层 | 生成随机强密码,确保密码包含多种字符类型 |
| PasswordManagerGUI | 界面层 | 主界面类,包含组件初始化、事件绑定、表格数据展示 |
| AddPasswordDialog | 界面层 | 添加密码对话框,处理密码添加的用户输入和提交 |
| EditPasswordDialog | 界面层 | 修改密码对话框,处理密码修改的用户输入和提交 |
| JHintTextField | 界面层 | 自定义带占位符的文本框,兼容JDK 8 |
3.3 数据存储设计
数据存储采用JSON格式,文件路径为System.getProperty("user.home") + "/passwords.json"(用户主目录),存储结构为密码条目数组,示例如下:
[ { "website": "淘宝", "account": "user123@taobao.com", "password": "加密后的密码字符串" }, { "website": "微信", "account": "13800138000", "password": "加密后的密码字符串" } ]采用JSON格式的优势:可读性强、易于解析、跨平台兼容,借助Gson库可快速实现Java对象与JSON字符串的转换。
四、算法说明
4.1 AES对称加密算法
本项目采用AES-128加密算法(CBC模式,PKCS5Padding填充)对密码进行加密,核心参数:
- 密钥(KEY):固定16位字符串(可自定义),示例:
1234567890abcdef; - 初始化向量(IV):固定16位字符串(可自定义),示例:
abcdef1234567890; - 字符编码:UTF-8;
- 加密结果:Base64编码后的字符串,便于存储和传输。
加密流程:
- 将明文密码转换为UTF-8编码的字节数组;
- 通过密钥和IV初始化AES加密器(ENCRYPT_MODE);
- 对字节数组进行加密,得到加密后的字节数组;
- 将加密后的字节数组转换为Base64字符串,作为最终存储的密码。
解密流程:
- 将Base64编码的加密密码解码为字节数组;
- 通过密钥和IV初始化AES解密器(DECRYPT_MODE);
- 对字节数组进行解密,得到明文密码的字节数组;
- 将字节数组转换为UTF-8编码的字符串,得到原始密码。
4.2 随机强密码生成算法
强密码生成算法确保生成的密码包含大写字母、小写字母、数字、特殊符号四类字符,且长度可自定义(≥8位),核心步骤:
- 定义四类字符的字符集:大写字母(A-Z)、小写字母(a-z)、数字(0-9)、特殊符号(!@#$%^&*()_+-=[]{}|;:,.<>?);
- 确保密码至少包含每类字符各一个;
- 填充剩余长度的随机字符(从所有字符集中随机选取);
- 打乱密码字符顺序,避免前四位固定为四类字符的顺序;
- 返回最终生成的随机密码。
注意:密码长度建议不小于8位,长度越长,密码安全性越高;特殊符号的加入可大幅提升密码的抗破解能力。
五、测试说明
5.1 测试环境
| 测试项 | 测试环境 |
|---|---|
| JDK版本 | JDK 8(1.8.0_301) |
| 操作系统 | Windows 10 / macOS 14 / Ubuntu 22.04 |
| 依赖库 | Gson 2.8.9(JSON解析) |
5.2 功能测试用例
| 测试用例ID | 测试功能 | 测试步骤 | 预期结果 | 测试结果 |
|---|---|---|---|---|
| TC001 | 添加密码 | 1. 点击“添加密码”按钮;2. 输入网站“测试网站”、账号“test@163.com”、密码“123456”;3. 点击“保存” | 提示“添加成功”,表格中显示该条目,passwords.json文件新增该记录(密码为加密后字符串) | 通过 |
| TC002 | 查询密码 | 1. 在搜索框输入“测试”;2. 点击“查询”按钮 | 表格仅显示包含“测试”关键词的密码条目 | 通过 |
| TC003 | 修改密码 | 1. 选择“测试网站”条目;2. 点击“修改密码”;3. 输入新密码“654321”;4. 点击“保存修改” | 提示“修改成功”,表格中该条目密码更新为新的加密字符串,原密码可通过“显示原密码”查看 | 通过 |
| TC004 | 删除密码 | 1. 选择“测试网站”条目;2. 点击“删除密码”;3. 确认删除 | 提示“删除成功”,表格中该条目消失,passwords.json文件中该记录被删除 | 通过 |
| TC005 | 生成强密码 | 1. 点击“生成强密码”;2. 输入长度“12”;3. 确认生成 | 弹出窗口显示12位随机密码(包含大小写、数字、特殊符号),密码可复制到剪贴板 | 通过 |
| TC006 | 加密解密验证 | 1. 添加密码“123456”;2. 解密加密后的字符串 | 解密结果与原始密码一致 | 通过 |
5.3 边界测试
- 密码长度测试:生成长度为8位、16位、32位的密码,验证生成结果符合规则;
- 空输入测试:添加密码时网站/账号/密码为空,验证系统提示“不能为空”;
- 关键词为空测试:查询框为空时,点击查询,显示所有密码条目;
- 无效数字测试:生成密码时输入非数字长度(如“abc”),验证系统提示“请输入有效的数字”;
- 短密码测试:生成密码时输入长度“7”,验证系统提示“密码长度不能小于8位”。
六、关键代码
6.1 AES加密解密工具类(AESUtil.java) import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; /** * AES加密工具类(兼容JDK8,CBC模式,PKCS5Padding填充) */ public class AESUtil { // 加密密钥(建议替换为自己的密钥,长度必须是16位(AES-128)、24位(AES-192)或32位(AES-256)) private static final String KEY = "1234567890abcdef"; // 初始化向量(IV),长度必须是16位 private static final String IV = "abcdef1234567890"; /** * AES加密 * @param content 要加密的内容 * @return 加密后的Base64字符串 * @throws Exception 加密异常 */ public static String encrypt(String content) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES"); IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } /** * AES解密 * @param content 加密后的Base64字符串 * @return 解密后的原始字符串 * @throws Exception 解密异常 */ public static String decrypt(String content) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES"); IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); byte[] decoded = Base64.getDecoder().decode(content); byte[] decrypted = cipher.doFinal(decoded); return new String(decrypted, StandardCharsets.UTF_8); } } 6.2 强密码生成工具类(PasswordGenerator.java) import java.util.Random; /** * 随机强密码生成工具类 */ public class PasswordGenerator { // 密码包含的字符:大写字母、小写字母、数字、特殊符号 private static final String UPPER_CASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private static final String LOWER_CASE = "abcdefghijklmnopqrstuvwxyz"; private static final String NUMBERS = "0123456789"; private static final String SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?"; private static final String ALL_CHARS = UPPER_CASE + LOWER_CASE + NUMBERS + SYMBOLS; private static final Random RANDOM = new Random(); /** * 生成随机强密码 * @param length 密码长度(建议至少8位) * @return 随机密码 */ public static String generateStrongPassword(int length) { if (length < 8) { throw new IllegalArgumentException("密码长度不能小于8位"); } StringBuilder password = new StringBuilder(); // 确保包含至少一种大写、小写、数字、特殊符号 password.append(UPPER_CASE.charAt(RANDOM.nextInt(UPPER_CASE.length()))); password.append(LOWER_CASE.charAt(RANDOM.nextInt(LOWER_CASE.length()))); password.append(NUMBERS.charAt(RANDOM.nextInt(NUMBERS.length()))); password.append(SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()))); // 填充剩余字符 for (int i = 4; i < length; i++) { password.append(ALL_CHARS.charAt(RANDOM.nextInt(ALL_CHARS.length()))); } // 打乱字符顺序(避免前四位固定为大写、小写、数字、符号) return shuffleString(password.toString()); } /** * 打乱字符串顺序 * @param str 原始字符串 * @return 打乱后的字符串 */ private static String shuffleString(String str) { char[] chars = str.toCharArray(); for (int i = chars.length - 1; i > 0; i--) { int j = RANDOM.nextInt(i + 1); char temp = chars[i]; chars[i] = chars[j]; chars[j] = temp; } return new String(chars); } } 6.3 数据存储工具类(PasswordStorage.java) import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.*; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; /** * 密码数据存储工具类,处理本地JSON文件的读写 */ public class PasswordStorage { // 数据存储的本地文件路径(用户主目录下的passwords.json) private static final String STORAGE_FILE = System.getProperty("user.home") + File.separator + "passwords.json"; private static final Gson gson = new Gson(); private static final Type LIST_TYPE = new TypeToken>() {}.getType(); /** * 读取所有密码信息 * @return 密码列表 */ public static List loadPasswords() { File file = new File(STORAGE_FILE); if (!file.exists()) { return new ArrayList<>(); } try (Reader reader = new FileReader(file)) { return gson.fromJson(reader, LIST_TYPE); } catch (Exception e) { e.printStackTrace(); return new ArrayList<>(); } } /** * 保存密码列表到本地文件 * @param entries 密码列表 * @return 是否保存成功 */ public static boolean savePasswords(List entries) { try (Writer writer = new FileWriter(STORAGE_FILE)) { gson.toJson(entries, writer); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 添加密码条目 * @param entry 密码条目 * @return 是否添加成功 */ public static boolean addPassword(PasswordEntry entry) { List entries = loadPasswords(); entries.add(entry); return savePasswords(entries); } /** * 根据网站和账号删除密码条目 * @param website 网站 * @param account 账号 * @return 是否删除成功 */ public static boolean deletePassword(String website, String account) { List entries = loadPasswords(); boolean removed = entries.removeIf(e -> e.getWebsite().equals(website) && e.getAccount().equals(account)); if (removed) { return savePasswords(entries); } return false; } /** * 根据网站和账号修改密码 * @param website 网站 * @param account 账号 * @param newPassword 新密码(加密后的) * @return 是否修改成功 */ public static boolean updatePassword(String website, String account, String newPassword) { List entries = loadPasswords(); for (PasswordEntry entry : entries) { if (entry.getWebsite().equals(website) && entry.getAccount().equals(account)) { entry.setPassword(newPassword); return savePasswords(entries); } } return false; } /** * 查询密码条目(支持模糊查询网站或账号) * @param keyword 关键词 * @return 匹配的密码列表 */ public static List searchPasswords(String keyword) { List entries = loadPasswords(); List result = new ArrayList<>(); for (PasswordEntry entry : entries) { if (entry.getWebsite().contains(keyword) || entry.getAccount().contains(keyword)) { result.add(entry); } } return result; } } 6.4 自定义占位符文本框(JHintTextField.java) import javax.swing.*; import java.awt.*; /** * 自定义带占位符的文本框(兼容JDK8,修复尺寸和输入问题) */ public class JHintTextField extends JTextField { private String hint; // 占位符文字 // 构造方法1:仅指定占位符,使用默认列数20 public JHintTextField(String hint) { this(hint, 20); // 默认20列,确保有足够宽度 } // 构造方法2:指定占位符和列数(推荐使用) public JHintTextField(String hint, int columns) { super(columns); // 调用父类的列数构造方法,设置文本框列数 this.hint = hint; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 如果文本框为空,绘制占位符 if (getText().isEmpty() && hint != null) { Graphics2D g2 = (Graphics2D) g; g2.setColor(Color.GRAY); // 占位符文字颜色 // 调整占位符的绘制位置,与文本框的默认文字对齐 int y = g.getFontMetrics().getAscent() + (getHeight() - g.getFontMetrics().getHeight()) / 2; g2.drawString(hint, getInsets().left, y); } } // 可选:重写首选尺寸,确保占位符文字能完整显示 @Override public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); if (hint != null) { FontMetrics fm = getFontMetrics(getFont()); int hintWidth = fm.stringWidth(hint) + getInsets().left + getInsets().right + 10; // 如果占位符宽度大于默认尺寸,使用占位符宽度 size.width = Math.max(size.width, hintWidth); } return size; } }