I recently updated a Spring Boot application from v1.5 to v2.0.3. It's an application with methods exposed as REST endpoints and secured by Basic HTTP authentication. The usernames and passwords are hardcoded in a properties file loaded by the application.
Since the update, the response time increased by almost 200ms, and 98% of the time processing a request is spent in BasicAuthenticationFilter.doFilter().
More specifically, time is spent encoding the password in the request to compare it with the password provided by configuration.
Here's an extract of the SecurityConfig class:
@EnableWebSecurity
@PropertySource("classpath:auth/auth.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${user.name}")
private String userName;
@Value("${user.password}")
private String userPassword;
@Value("${user.roles}")
private String[] userRoles;
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails user = User.withUsername(userName).password(encoder.encode(userPassword)).roles(userRoles).build();
auth.inMemoryAuthentication().withUser(user);
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
//make sure that the basic authentication header is always required
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//set the correct authentication entry point to make sure the 401 response is in JSON format
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint());
//enable http basic authentication
http.httpBasic();
//make sure authentication is required for all requests except /actuator/info
http.authorizeRequests().antMatchers("/actuator/info").permitAll().anyRequest().authenticated();
http.authorizeRequests().antMatchers("/actuator/**").hasAnyRole("MONITOR", "ACTUATOR");
//disable the form login since this is a REST API
http.formLogin().disable();
//disable csrf since this is a REST API
http.csrf().disable();
}
}
To verify that it was due to the Spring Boot update, I locally reverted the changes and ran some tests. The response time was divided by 4.
I've tried a few things already but none of them improved the response time:
Can I do anything to speed up the authentication filter?
bcrypt is an intentionally slow hashing function. While this slowness sounds paradoxical when it comes to password hashing it isn't because both the good guys and the bad are slowed down. These days most password attacks are some variant of a brute force dictionary attack. This means that an attacker will try many, many candidate passwords by hashing them just like the good guys do. If there is a match, the password has been cracked.
However, if the bad guy is slowed down per try that is amplified when millions and millions of attempts are made, often to the point of thwarting the attack. Yet the good guys probably won't notice on a single attempt when they try to log in.
So basically the extra 200ms processing time inside the brypt encoder is intentional and IS PART OF THE SECURITY By speeding it up (like using cache) you decrease the security level of your application.
--- edit:
Btw. There is still a quick AND secure way to evaluate the password-match: Use some cache-service (like in other answers here), but store only the matched values in the cache!!! This way if a user provides a valid password, it will be evaluated slowly only once - at the very first time - but all of his subsequent logins will be quick. But if an attacker tries using brute-force, then all of his attempts will take 200ms.
I had a similar problem, and fixed it by wrapping caching around the password encoder (which could be used in the previous answer). Assuming the same user ids are used frequently this should solve the problem. It uses the Caffeine caching library.
private static class CachingPasswordEncoder implements PasswordEncoder {
private final PasswordEncoder encoder = new BCryptPasswordEncoder();
private final Cache<CharSequence, String> encodeCache = Caffeine.newBuilder().build();
private final Cache<PasswordMatchKey, Boolean> matchCache = Caffeine.newBuilder().build();
@Override
public String encode(final CharSequence rawPassword) {
return encodeCache.get(rawPassword, s -> encoder.encode(rawPassword));
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
//noinspection ConstantConditions
return matchCache.get(new PasswordMatchKey(rawPassword, encodedPassword),
k -> encoder.matches(rawPassword, encodedPassword));
}
}
private static class PasswordMatchKey {
private final CharSequence rawPassword;
private final String encodedPassword;
public PasswordMatchKey(CharSequence rawPassword, String encodedPassword) {
this.rawPassword = rawPassword;
this.encodedPassword = encodedPassword;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PasswordMatchKey that = (PasswordMatchKey) o;
return Objects.equals(rawPassword, that.rawPassword) &&
Objects.equals(encodedPassword, that.encodedPassword);
}
@Override
public int hashCode() {
return Objects.hash(rawPassword, encodedPassword);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With