解决Spring security5.5.7报错Encoded password does not look like BCrypt异常
作者:kevingavinhu
背景
一个老项目,由于2022年爆发了spring bean和spring core的漏洞,将springboot从1.5.4升级到2.5.14版本,修复了spring的漏洞。
同时spring security也需要同步升级,升级过程中出现了一系列错误。
故做一个记录在此。
问题:
登录权限系统时,出现Encoded password does not look like BCrypt异常错误;同时报出clientSecret不匹配的问题。
解决方案
统一采用PasswordEncoderFactories.createDelegatingPasswordEncoder()去获取到密码加密器PasswordEncoder
(1)新版本的Spring Security对客户端的密钥进行了加密处理,配置中需要使用PasswordEncoderFactories.createDelegatingPasswordEncoder().encode进行加密;
(2)登录成功后的handler,则需要采用PasswordEncoderFactories.createDelegatingPasswordEncoder().matches(clientSecret,clientDetails.getClientSecret())判断密钥是否匹配,而不是采用原有旧版的未加密的密钥进行equal进行比较字符串。
BCryptPasswordEncoder介绍
Spring Security 中提供了 BCryptPasswordEncoder用于用户密码的加密和验证,这里讲解一下该 PasswordEncoder 的实现逻辑.
首先 BCryptPasswordEncoder 使用了 BCrypt 算法来对密码实现加密和验证。由于 BCrypt本身是一种 单向Hash算法,因此它和我们日常用的 MD5一样,通常情况下是无法逆向解密的。
在 BSD系统中 BCrypt 算法主要用来替代 md5 加密算法,它使用了一种可变版本的Blowfish流密码算法。通过多次加盐和随机数,因此这套加密算法被广泛用于许多系统的密码加密当中。
然而每次加密的结果是不一样的,如果采用两次加密的结果进行equal比较,那是得不到真实的true结果的。
具体代码改动
- 注册一个bean,覆盖原有的PasswordEncoder
/** * 加密方式,spring security5.5.7升级后,默认采用BCryptPasswordEncoder * @return */ @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
- 项目启动时,加载的认证服务器配置的修改
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { /** * 引入BCrypt强哈希加密和解密工具 */ @Autowired private PasswordEncoder passwordEncoder; /* * 客户端配置改动 * * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { InMemoryClientDetailsServiceBuilder builder = clients.inMemory(); //spirng boot 1.5.* 升级到spring boot 2.0以上,当再次访问授权服务器时出现Encoded password does not look like BCrypt异常,需要passwordEncoder.encode if (ArrayUtils.isNotEmpty(securityProperties.getOauth2().getClients())) { for (OAuth2ClientProperties config : securityProperties.getOauth2().getClients()) { //设置clientid builder.withClient(config.getClientId()) // 设置clientsecret,需要加密配置 .secret(passwordEncoder.encode(config.getClientSecret())) // 设置令牌过期时间,单位秒,默认7200,定义在OAuth2ClientProperties .accessTokenValiditySeconds(config.getAccessTokenValidateSeconds()) // 允许的授权模式 .authorizedGrantTypes("refresh_token", "authorization_code", "password") // 设置刷新令牌的过期时间,单位秒,这里设置为60天 .refreshTokenValiditySeconds(5184000) // 配置oauth能获取的权限,是一个数组 .scopes("all", "write", "read"); } } }
- 构造用户登录信息
@Component public class MyUserDetailsService implements UserDetailsService { /** * 注入加密器 */ @Autowired private PasswordEncoder passwordEncoder; /** * 搜索用户信息,构造登录用户 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //其他数据库查询等逻辑省略........ logger.info("表单登录用户名:" + username); //进行权限系统登录,密码前面需要加上加密的方式,调用createDelegatingPasswordEncoder后默认会加上{bcrypt}在密码前面 String password = passwordEncoder.encode("123456"); user.setLastLoginTime(nowTime); sysPublicUserRepository.save(user); logger.info("保存登录时间:" + nowTime); User user1 = new User(userId, password, true, accountNonExpired, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles())); return user1; } }
- 登录成功后SuccessHandler的校验
@Component("authenticationSuccessHandler") public class authenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { /** * 注入密码器 */ @Autowired private PasswordEncoder passwordEncoder; /** * 此方法是用于在request中取得ClientDetails和新建tokenRequest,并用这两个参数来生成OAuth2AccessToken */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //..............省略代码 //.............. //客户端的密钥,从header中取出 String clientSecret = extractAndDecodeHeader(header, request); ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); if (clientDetails == null) { throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在:" + clientId); } else if (!passwordEncoder.matches(clientSecret,clientDetails.getClientSecret())) { //关键是这里不能用equal匹配了 throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId); } //............ //后续token的其他处理 //............ } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。