Bladeren bron

Lazily initialize userNotFoundEncodedPassword

Update `DaoAuthenticationProvider` so that `userNotFoundEncodedPassword`
is lazily initialized on the first call to `retrieveUser`, rather than
in `doAfterPropertiesSet`.

Since some `PasswordEncoder` implementations can be slow, this change
can help to improve application startup times and the expense of some
delay with the first login.

Note that `userNotFoundEncodedPassword` creation occurs on the first
user retrieval, regardless of whether the user is ultimately found. This
ensures consistent processing times, regardless of the outcome.

First Call:
	Found      = encode(userNotFound) + decode(supplied)
	Not-Found  = encode(userNotFound) + decode(userNotFound)

Subsequent Call:
	Found      = decode(supplied)
	Not-Found  = decode(userNotFound)

Fixes gh-4915
Phillip Webb 7 jaren geleden
bovenliggende
commit
fd78d055aa

+ 27 - 18
core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java

@@ -58,7 +58,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
 	 * {@link PasswordEncoder} implementations will short circuit if the password is not
 	 * in a valid format.
 	 */
-	private String userNotFoundEncodedPassword;
+	private volatile String userNotFoundEncodedPassword;
 
 	private UserDetailsService userDetailsService;
 
@@ -94,34 +94,43 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
 
 	protected void doAfterPropertiesSet() throws Exception {
 		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
-		this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
 	}
 
 	protected final UserDetails retrieveUser(String username,
 			UsernamePasswordAuthenticationToken authentication)
 			throws AuthenticationException {
-		UserDetails loadedUser;
-
+		prepareTimingAttackProtection();
 		try {
-			loadedUser = this.getUserDetailsService().loadUserByUsername(username);
-		}
-		catch (UsernameNotFoundException notFound) {
-			if (authentication.getCredentials() != null) {
-				String presentedPassword = authentication.getCredentials().toString();
-				passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
+			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
+			if (loadedUser == null) {
+				throw new InternalAuthenticationServiceException(
+						"UserDetailsService returned null, which is an interface contract violation");
 			}
-			throw notFound;
+			return loadedUser;
+		}
+		catch (UsernameNotFoundException ex) {
+			mitigateAgainstTimingAttack(authentication);
+			throw ex;
+		}
+		catch (InternalAuthenticationServiceException ex) {
+			throw ex;
 		}
-		catch (Exception repositoryProblem) {
-			throw new InternalAuthenticationServiceException(
-					repositoryProblem.getMessage(), repositoryProblem);
+		catch (Exception ex) {
+			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
 		}
+	}
+
+	private void prepareTimingAttackProtection() {
+		if (this.userNotFoundEncodedPassword == null) {
+			this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
+		}
+	}
 
-		if (loadedUser == null) {
-			throw new InternalAuthenticationServiceException(
-					"UserDetailsService returned null, which is an interface contract violation");
+	private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
+		if (authentication.getCredentials() != null) {
+			String presentedPassword = authentication.getCredentials().toString();
+			this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
 		}
-		return loadedUser;
 	}
 
 	/**