登录失败与 OAuth2 的 invalid_client 都可能涉及密码校验,但认证的不是同一个身份。用户密码属于用户认证,client secret 属于客户端认证,只修一处并不等于两条链都恢复。
用户密码的 id 为 null
2019 年一次内存用户配置报错:
There is no PasswordEncoder mapped for the id "null"
DelegatingPasswordEncoder 通过 {id}encodedPassword 的前缀选择算法。裸 BCrypt 常以 $2a$ 等标识开头,但它不是委派编码器需要的 {bcrypt} 前缀。不能仅凭“已经加密”推断任意编码器都能读取。
下面用同一个委派编码器生成并匹配存储值:
org.springframework.security.crypto.password.PasswordEncoder encoder =
org.springframework.security.crypto.factory
.PasswordEncoderFactories.createDelegatingPasswordEncoder();
String encodedForStorage = encoder.encode("example-only-password");
boolean matches = encoder.matches("example-only-password", encodedForStorage);
这是教学输入,真实凭据不写死在代码或日志中。若直接使用 BCryptPasswordEncoder,存储形式也应符合它的契约;不能对任意旧字符串机械加前缀。Spring Security 密码格式
client secret 是另一处配置
另一条记录是用户密码调整后,客户端仍报 invalid_client、Bad client credentials。后来将客户端注册时的 secret 按对应编码器处理,流程才恢复。
旧 Spring Security OAuth 的内存用户与客户端注册信息有不同配置入口。需要分别核对 UserDetails 的密码和 ClientDetails 的 secret,而不是认为一个 PasswordEncoder Bean 自动修好了所有历史记录。invalid_client 还可能涉及 client_id、认证方式或参数位置,应沿客户端认证环节定位。
匹配不是再次编码后比较字符串
客户端通常提交原始凭据,服务端读取已保存的编码值,用 matches(raw, encoded) 比较。密码哈希可能带随机盐,两次 encode 得到不同字符串是正常现象,不应把两个结果直接用等号比较。
也不要把编码后的存储值当作客户端应提交的 secret。既有明文或旧哈希迁移应有明确方案,不应为了消除异常把全部记录改为 {noop} 或关闭校验。
分开验证两条链
先确认用户能认证,再确认客户端身份、secret 格式和认证方式,最后看授权及 token 获取。日志可记录失败环节和算法标识,不打印密码、secret 或完整认证头。这里恢复的是旧框架排障思路,不是生产凭据管理方案。