JEE(JSF)迁Spring Boot:数据库未加密密码优雅加密方案咨询
存量明文密码批量加密方案(基于Spring Boot CommandLineRunner)
核心思路
利用Spring Boot的CommandLineRunner Bean,在应用启动时自动检测数据库中未加密的用户密码,通过Spring Security提供的加密器批量加密并更新回数据库,同时加入迁移状态标记避免重复执行,全程无需手动操作。
具体实现
1. 调整用户实体类
新增密码格式判断方法,用于区分明文与加密后的密码(以BCrypt为例,加密后的密码以$2a$/$2b$/$2y$开头):
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; // 其他业务字段... // 判断密码是否为明文 public boolean isPasswordPlainText() { return !password.startsWith("$2a$") && !password.startsWith("$2b$") && !password.startsWith("$2y$"); } // getter & setter public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
2. 扩展用户Repository
添加查询所有明文密码用户的方法(支持分页避免内存溢出):
public interface UserRepository extends JpaRepository<User, Long> { @Query("SELECT u FROM User u WHERE u.password NOT LIKE '$2a$%' AND u.password NOT LIKE '$2b$%' AND u.password NOT LIKE '$2y$%'") Page<User> findAllWithPlainTextPasswords(Pageable pageable); }
3. 迁移状态标记实体(可选但推荐)
创建专门的迁移状态表,避免每次启动重复执行加密逻辑:
@Entity @Table(name = "migration_status") public class MigrationStatus { @Id private String id; // 用固定值如"password_encryption"作为主键 private boolean completed; // getter & setter public String getId() { return id; } public void setId(String id) { this.id = id; } public boolean isCompleted() { return completed; } public void setCompleted(boolean completed) { this.completed = completed; } }
对应的Repository:
public interface MigrationStatusRepository extends JpaRepository<MigrationStatus, String> { }
4. 实现CommandLineRunner迁移逻辑
@Component public class PasswordMigrationRunner implements CommandLineRunner { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final MigrationStatusRepository migrationStatusRepository; // 构造函数注入(Spring 4.3+自动支持) public PasswordMigrationRunner(UserRepository userRepository, PasswordEncoder passwordEncoder, MigrationStatusRepository migrationStatusRepository) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.migrationStatusRepository = migrationStatusRepository; } @Override public void run(String... args) throws Exception { // 检查迁移是否已完成 MigrationStatus status = migrationStatusRepository.findById("password_encryption").orElse(null); if (status != null && status.isCompleted()) { System.out.println("密码加密迁移已完成,跳过执行"); return; } // 分页处理明文密码用户 int pageSize = 100; int pageNum = 0; int totalProcessed = 0; Page<User> userPage; do { userPage = userRepository.findAllWithPlainTextPasswords(PageRequest.of(pageNum, pageSize)); if (userPage.isEmpty()) break; userPage.getContent().forEach(user -> { String encryptedPwd = passwordEncoder.encode(user.getPassword()); user.setPassword(encryptedPwd); }); userRepository.saveAll(userPage.getContent()); totalProcessed += userPage.getNumberOfElements(); pageNum++; } while (userPage.hasNext()); // 标记迁移完成 if (status == null) { status = new MigrationStatus(); status.setId("password_encryption"); } status.setCompleted(true); migrationStatusRepository.save(status); System.out.println("密码加密迁移完成,共处理" + totalProcessed + "个用户"); } }
5. 配置Spring Security加密器
@Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { // 使用Spring Security推荐的BCrypt强哈希加密器 return new BCryptPasswordEncoder(); } // 其他Spring Security认证配置(如UserDetailsService、AuthenticationManager等) }
关键注意事项
- 数据库备份:执行前务必备份用户表,避免数据异常丢失。
- 环境测试:先在测试环境验证加密逻辑,确保加密后用户能正常登录。
- 算法适配:若旧系统使用特定加密算法(如MD5),需替换对应的
PasswordEncoder(如MessageDigestPasswordEncoder("MD5")),但推荐优先升级到BCrypt等强算法。 - 性能优化:用户量较大时,通过分页查询分批次处理,避免一次性加载大量数据占用内存。
内容的提问来源于stack exchange,提问作者azerELweed
相关产品推荐
相关产品推荐

