Spring Security + JWT |比较密码时出现假阴性
Posted
技术标签:
【中文标题】Spring Security + JWT |比较密码时出现假阴性【英文标题】:Spring Security + JWT | False negative when comparing passwords 【发布时间】:2021-10-22 08:04:06 【问题描述】:这是我在这个问题上的第三次更新:
Spring 的 AuthenticationManager 对我的普通字符串“密码”和编码的“密码”字符串执行 .matches() 方法,并且每次都返回 false,即使它应该返回 true。我已将其配置为使用 BCrypt,但我不知道我是否遗漏了什么......
我正在添加我的 SecurityConfig、DaoAuthProvider 和调试器图片:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
@Autowired
private MyUserDetailsService userService;
@Autowired
JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(userService)
.passwordEncoder(passwordEncoder());
@Override
protected void configure(HttpSecurity http) throws Exception
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate","/register").permitAll()
.anyRequest().authenticated()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
@Bean
public PasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder();
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider()
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException
if (authentication.getCredentials() == null)
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
else
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
protected void doAfterPropertiesSet()
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException
this.prepareTimingAttackProtection();
try
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null)
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
else
return loadedUser;
catch (UsernameNotFoundException var4)
this.mitigateAgainstTimingAttack(authentication);
throw var4;
catch (InternalAuthenticationServiceException var5)
throw var5;
catch (Exception var6)
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user)
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding)
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
return super.createSuccessAuthentication(principal, authentication, user);
private void prepareTimingAttackProtection()
if (this.userNotFoundEncodedPassword == null)
this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication)
if (authentication.getCredentials() != null)
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
public void setPasswordEncoder(PasswordEncoder passwordEncoder)
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
protected PasswordEncoder getPasswordEncoder()
return this.passwordEncoder;
public void setUserDetailsService(UserDetailsService userDetailsService)
this.userDetailsService = userDetailsService;
protected UserDetailsService getUserDetailsService()
return this.userDetailsService;
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService)
this.userDetailsPasswordService = userDetailsPasswordService;
调试器图片
1:What i see in the debugger at the time of the password validation
【问题讨论】:
【参考方案1】:如 spring 安全文档的密码部分所述,问题很可能是因为您没有在密码前添加要使用的解码器。
因为您使用的是PasswordEncoderFactories.createDelegatingPasswordEncoder()
你可以在createDelegatingPasswordEncoder源代码中看到,你得到的是一个DelegatingPasswordEncoder
,它基本上包括一个映射,带有几个密码编码器的密钥。
如果我们随后查看DelegatingPasswordEncoder#matches 函数的源代码第一行,我们可以看到它想从密码中提取一个id 来识别要使用的编码器。
如果您阅读了spring security reference on passwords,它会非常清楚地说明密码在 Spring Security 中的工作原理,以及 DelegatingPasswordEncoder
的工作原理和 what specific format
的使用方式。
Password Storage Formats
在数据库中静态存储时,密码需要在前面加上 id。
idencodedPassword
取自文档的示例密码。
bcrypt$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
nooppassword
pbkdf25d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
scrypt$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
sha25697cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
仅当您使用DelegatingPasswordEncoder
时才需要这种格式,因为这样可以确保您可以将不同编码格式的密码存储在数据库中。
如果您想忽略此格式,请直接使用BCryptPasswordEncoder
。并将@Autowire
加入课堂。
我建议您阅读 spring 安全参考中有关密码的整个部分,阅读时间为 10 分钟,可以节省您和我的时间。
另外,我只想指出,因为如果我不至少提到这是writing custom security as you have done is bad practice
,那就太粗心了。 Spring has fully tested and customizable JWT support since 3 years back,因此不需要构建自定义 JWTFilter,它还具有完全构建和可自定义的 DaoAuthenticationProviders。
Spring 安全性已经过全面测试,并已在全球数千个应用程序中使用。如果您不打算利用其中的功能,为什么还要引入安全库。
此外,您的代码中的一个错误可能会危及您的整个应用程序及其所有数据,请确保您愿意在将其投入生产之前承担该风险。
【讨论】:
以上是关于Spring Security + JWT |比较密码时出现假阴性的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot 2 + Spring Security 5 + JWT 的 Restful简易教程!
Spring Security + JWT 实现单点登录,还有谁不会。。。
#私藏项目实操分享# Spring专题「开发实战」Spring Security与JWT实现权限管控以及登录认证指南
springCloud-依赖Spring Security使用 JWT实现无状态的分布式会话