diff --git a/core/src/main/java/org/springframework/security/core/userdetails/User.java b/core/src/main/java/org/springframework/security/core/userdetails/User.java index eb8cb53d85..69a645c6e4 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/User.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/User.java @@ -27,11 +27,16 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; /** @@ -58,6 +63,8 @@ public class User implements UserDetails, CredentialsContainer { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private static final Log logger = LogFactory.getLog(User.class); + // ~ Instance fields // ================================================================================================ private String password; @@ -244,8 +251,99 @@ public class User implements UserDetails, CredentialsContainer { return sb.toString(); } + /** + * Creates a UserBuilder with a specified user name + * + * @param username the username to use + * @return the UserBuilder + */ public static UserBuilder withUsername(String username) { - return new UserBuilder().username(username); + return builder().username(username); + } + + /** + * Creates a UserBuilder + * + * @return the UserBuilder + */ + public static UserBuilder builder() { + return new UserBuilder(); + } + + /** + *

+ * WARNING: This method is considered unsafe for production and is only intended + * for sample applications. + *

+ *

+ * Creates a user and automatically encodes the provided password using + * {@code PasswordEncoderFactories.createDelegatingPasswordEncoder()}. For example: + *

+ * + *
+	 * 
+	 * UserDetails user = User.withDefaultPasswordEncoder()
+	 *     .username("user")
+	 *     .password("password")
+	 *     .roles("USER")
+	 *     .build();
+	 * // outputs {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+	 * System.out.println(user.getPassword());
+	 * 
+	 * 
+ * + * This is not safe for production (it is intended for getting started experience) + * because the password "password" is compiled into the source code and then is + * included in memory at the time of creation. This means there are still ways to + * recover the plain text password making it unsafe. It does provide a slight + * improvement to using plain text passwords since the UserDetails password is + * securely hashed. This means if the UserDetails password is accidentally exposed, + * the password is securely stored. + * + * In a production setting, it is recommended to hash the password ahead of time. + * For example: + * + *
+	 * 
+	 * PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+	 * // outputs {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+	 * // remember the password that is printed out and use in the next step
+	 * System.out.println(encoder.encode("password"));
+	 * 
+	 * 
+ * + *
+	 * 
+	 * UserDetails user = User.withUsername("user")
+	 *     .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
+	 *     .roles("USER")
+	 *     .build();
+	 * 
+	 * 
+ * + * @return a UserBuilder that automatically encodes the password with the default + * PasswordEncoder + * @deprecated Using this method is not considered safe for production, but is + * acceptable for demos and getting started. For production purposes, ensure the + * password is encoded externally. See the method Javadoc for additional details. + * There are no plans to remove this support. It is deprecated to indicate + * that this is considered insecure for production purposes. + */ + @Deprecated + public static UserBuilder withDefaultPasswordEncoder() { + logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications."); + PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + return builder().passwordEncoder(encoder); + } + + public static UserBuilder withUserDetails(UserDetails userDetails) { + return withUsername(userDetails.getUsername()) + .password(userDetails.getPassword()) + .accountExpired(!userDetails.isAccountNonExpired()) + .accountLocked(!userDetails.isAccountNonLocked()) + .authorities(userDetails.getAuthorities()) + .credentialsExpired(!userDetails.isCredentialsNonExpired()) + .disabled(!userDetails.isEnabled()); } /** @@ -260,6 +358,7 @@ public class User implements UserDetails, CredentialsContainer { private boolean accountLocked; private boolean credentialsExpired; private boolean disabled; + private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance(); /** * Creates a new instance @@ -274,7 +373,7 @@ public class User implements UserDetails, CredentialsContainer { * @return the {@link UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */ - private UserBuilder username(String username) { + public UserBuilder username(String username) { Assert.notNull(username, "username cannot be null"); this.username = username; return this; @@ -293,6 +392,20 @@ public class User implements UserDetails, CredentialsContainer { return this; } + /** + * Encodes the current password (if non-null) and any future passwords supplied + * to {@link #password(String)}. + * + * @param encoder the encoder to use + * @return the {@link UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + private UserBuilder passwordEncoder(PasswordEncoder encoder) { + Assert.notNull(encoder, "encoder cannot be null"); + this.passwordEncoder = encoder; + return this; + } + /** * Populates the roles. This method is a shortcut for calling * {@link #authorities(String...)}, but automatically prefixes each entry with @@ -351,7 +464,7 @@ public class User implements UserDetails, CredentialsContainer { * additional attributes for this user) * @see #roles(String...) */ - public UserBuilder authorities(List authorities) { + public UserBuilder authorities(Collection authorities) { this.authorities = new ArrayList(authorities); return this; } @@ -418,7 +531,8 @@ public class User implements UserDetails, CredentialsContainer { } public UserDetails build() { - return new User(username, password, !disabled, !accountExpired, + String encodedPassword = this.passwordEncoder.encode(password); + return new User(username, encodedPassword, !disabled, !accountExpired, !credentialsExpired, !accountLocked, authorities); } }