【问题标题】:Spring Security + JWT | False negative when comparing passwordsSpring Security + JWT |比较密码时出现假阴性
【发布时间】: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

【问题讨论】:

    标签: java spring jpa spring-security jwt


    【解决方案1】:

    如 spring 安全文档的密码部分所述,问题很可能是因为您没有在密码前添加要使用的解码器。

    因为您使用的是PasswordEncoderFactories.createDelegatingPasswordEncoder()

    你可以在createDelegatingPasswordEncoder源代码中看到,你得到的是一个DelegatingPasswordEncoder,它基本上包括一个映射,带有几个密码编码器的密钥。

    如果我们随后查看DelegatingPasswordEncoder#matches 函数的源代码第一行,我们可以看到它想从密码中提取一个id 来识别要使用的编码器。

    如果您阅读了spring security reference on passwords,它会非常清楚地说明密码在 Spring Security 中的工作原理,以及 DelegatingPasswordEncoder 的工作原理和 what specific format 的使用方式。

    Password Storage Formats

    在数据库中静态存储时,密码需要在前面加上 id。

    {id}encodedPassword
    

    取自文档的示例密码。

    {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
    {noop}password 
    {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 
    {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  
    {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 
    

    仅当您使用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 安全性已经过全面测试,并已在全球数千个应用程序中使用。如果您不打算利用其中的功能,为什么还要引入安全库。

    此外,您的代码中的一个错误可能会危及您的整个应用程序及其所有数据,请确保您愿意在将其投入生产之前承担该风险。

    【讨论】:

      猜你喜欢
      • 2014-07-29
      • 2019-06-27
      • 1970-01-01
      • 2015-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-25
      • 2014-02-14
      相关资源
      最近更新 更多