如何自定义BCryptPasswordEncoder覆盖PasswordEncoderFactories的默认配置
常见报错与基础解决方案
使用Spring Security时曾遇到如下500级报错:
There was an unexpected error (type=Internal Server Error, status=500). There is no PasswordEncoder mapped for the id "null" java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
该问题的核心原因是未显式配置密码编码器,简化配置场景下通常声明如下Bean即可解决:
@Bean PasswordEncoder encoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
在Spring Security 5.4.2版本中,createDelegatingPasswordEncoder()方法的底层实现逻辑如下:
@SuppressWarnings("deprecation") public static PasswordEncoder createDelegatingPasswordEncoder() { String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new LdapShaPasswordEncoder()); encoders.put("MD4", new Md4PasswordEncoder()); encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new StandardPasswordEncoder()); encoders.put("argon2", new Argon2PasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); }
其中默认注册的BCryptPasswordEncoder参数为:
- 版本标识:
$2a - 加密强度:
10
自定义BCrypt参数的适配方案
如果需要声明自定义参数的BCrypt编码器,例如:
PasswordEncoder bcryptEncoder = new BCryptPasswordEncoder(BCryptVersion.$2Y, 12);
需要实现将该自定义实例替换掉默认委托流程中绑定"bcrypt"标识的默认实现,同时完整保留其他所有内置编码器的配置。
需要注意DelegatingPasswordEncoder本身未提供公开方法修改内部已注册的编码器映射关系,可通过以下两种方式实现需求:
方案1:反射修改内部映射(低维护成本)
先通过工厂方法获取全量默认配置的委托编码器实例,再通过反射访问其内部存储编码器映射的私有字段,直接替换"bcrypt"键对应的编码器实现,无需手动维护全量编码器列表:
@Bean PasswordEncoder delegatingPasswordEncoder() throws NoSuchFieldException, IllegalAccessException { // 获取包含所有内置默认编码器的委托实例 DelegatingPasswordEncoder defaultEncoder = (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder(); // 反射获取内部存储编码器映射的私有字段 Field encodersField = DelegatingPasswordEncoder.class.getDeclaredField("idToPasswordEncoder"); encodersField.setAccessible(true); Map<String, PasswordEncoder> encoders = (Map<String, PasswordEncoder>) encodersField.get(defaultEncoder); // 替换bcrypt标识对应的自定义编码器 encoders.put("bcrypt", new BCryptPasswordEncoder(BCryptVersion.$2Y, 12)); return defaultEncoder; }
该方案的优势是可以自动跟随Spring Security版本迭代同步默认编码器配置,维护成本低;缺点是依赖类的私有字段实现,跨大版本升级时若字段名变更需要调整代码。
方案2:手动构建全量映射(无反射依赖)
完全基于公开API手动构建编码器映射表,将"bcrypt"对应的值替换为自定义实例,其余条目与默认工厂实现保持完全一致,再手动构造DelegatingPasswordEncoder返回:
@Bean PasswordEncoder delegatingPasswordEncoder() { String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); // 替换bcrypt条目为自定义参数实现 encoders.put(encodingId, new BCryptPasswordEncoder(BCryptVersion.$2Y, 12)); // 其余条目与默认工厂配置完全对齐 encoders.put("ldap", new LdapShaPasswordEncoder()); encoders.put("MD4", new Md4PasswordEncoder()); encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new StandardPasswordEncoder()); encoders.put("argon2", new Argon2PasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); }
该方案的优势是完全基于公开API实现,不存在反射带来的版本兼容风险;缺点是版本升级时需要手动同步Spring Security对应的默认编码器列表,避免出现配置遗漏。
两种方案实现后,DelegatingPasswordEncoder的整体行为与默认工厂创建的实例保持一致:新加密的密码依然会使用{bcrypt}作为编码标识前缀,所有内置编码器对历史格式密码的校验逻辑完全保留,仅替换了bcrypt算法对应的参数配置。
内容的提问来源于stack exchange,提问作者Manuel Jordan

