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实现无状态的分布式会话

社交登录,spring-security-oauth2 和 spring-security-jwt?

Spring Security----JWT详解