We’ve all most likely seen they boiler plate code for managing users in Spring. This post really won’t be that much different so feel free to skip this if you are already familiar. We will be building off of this code so if a subsequent post throws you for a loop please come back here.
What I really want to do is take away any of the magic I certainly felt when I first started using Spring for managing users. If we stand back and take a look at how Spring does this it’s straight forward. We have the concept of a user, roles that allow those users to do things (authorization) and an authentication mechanism. As we develop our implementation we will talk about the specific Spring classes that help us accomplish this. Also, to keep things simple we will storing data using our own database.
So let’s kick it off by asking the question, what defines a user? At a minimum, a user is a unique identifier that, well, identifies a person. Something that it is memorable like a nickname or combination of first name and last name. While there are certainly better guidelines out there for selecting a user name I do, at a minimum, highly suggest avoiding the use of email addresses as that identifier. To verify that the visitor to our site is who they say they are we also associate a password to that user. Something we let them pick when they register on our site. If our site has different levels of access a user has to be associated with various roles.
As the internet has evolved, we’ve required more and more from our users to protect their information while still providing them ease of use. For example, if someone forget their password we use their email address to securely allow them to reset it. If their password was stolen, we use multi-factor authentication to help protect their account.
For our first implementation we are going to handle users, roles and passwords. We will break out into separate posts for developing multi-factor authentication and email password resetting.
User and Roles
Below is our User and Role entities that we will use for storing user information and their associated authorizations. While we discussed other features, such as multi-factor authorization, we will add those to the below class in a separate tutorial.
@Entity public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column private String firstName; @Column private String lastName; @Column private String username; @Column private String password; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER) private Set<Role> roles; @Column private String email; @Column private boolean enabled; @Column private LocalDateTime creationDate; @Column private LocalDateTime lastModifiedDate; public User() { creationDate = LocalDateTime.now(); } public User(String firstName, String lastName, String email, String username, String password, boolean enabled) { creationDate = LocalDateTime.now(); this.firstName = firstName; this.lastName = lastName; this.email = email; this.username = username; this.password = password; this.enabled = enabled; } // Getters and setters not shown. }
@Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column private Long id; @JsonIgnore @ManyToOne private User user; @Column private String role; public Role() { } public Role(String role) { this.role = role; } // Getters and setters not shown. }
Customizing User Details Service
Next, let’s introduce our own implementation of UserDetailsService or its sub-class UserDetailsManager. We’ll unimaginatively call CustomUserDetailsManager for the sake of this tutorial. We will create a bean for it in our application config so it will get picked up by Spring. Also, we’ll provide the service a JPA repo used to read/write our user data to our database and a BCrypt password encoder instance to securely store passwords.
@Bean protected UserDetailsService userDetailsService() { return new CustomUserDetailsManager(userRepository, passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
Let me start with that on one hand I find the UserDetailsManager interface a bit strange. I’m so used to accessing repos directly to read and write data that this interface feels odd to me. Having said that, I like that it brings structure to the party for managing users. The base interface, UserDetailsService, simply defines the signature for loading users by username. While this is perfectly good enough for most needs, UserDetailsManager adds more methods that are going to be common when managing users. Using these interfaces allows us to hook into Spring and have one place for this code to live. This is useful since the last thing we want to do is accidentally write code to manage users in more than one location. Now, it’s time for us to move forward with the implementation of our CustomUserDetailsManager.
@Component public class CustomUserDetailsManager implements UserDetailsManager { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @Autowired public CustomUserDetailsManager(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (null == user) { throw new UsernameNotFoundException("No user present with username: " + username); } else { return new CustomUserDetails(user); } } @Override public void createUser(UserDetails userDetails) { User user = ((CustomUserDetails) userDetails).getUser(); user.setPassword(passwordEncoder.encode(user.getPassword())); userRepository.save(user); } @Override public void updateUser(UserDetails userDetails) { User user = ((CustomUserDetails) userDetails).getUser(); userRepository.save(user); } @Override public void deleteUser(String username) { CustomUserDetails details; try { details = (CustomUserDetails) loadUserByUsername(username); } catch (UsernameNotFoundException ex) { return; } User user = details.getUser(); user.setEnabled(false); userRepository.save(user); } @Override public void changePassword(String oldPassword, String newPassword) { Authentication currentUser = SecurityContextHolder.getContext() .getAuthentication(); CustomUserDetails details = (CustomUserDetails) currentUser.getDetails(); // Validate old password is correct. if (passwordEncoder.matches(oldPassword, details.getPassword())) { // The password was valid, go ahead and update this user's password and persist it. User user = details.getUser(); user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); } } @Override public boolean userExists(String s) { return userRepository.findByUsername(s) != null; } }
Customer User Details
Our implementation of CustomUserDetailsManager pulls in a new class we haven’t talked about yet and that’s CustomUserDetails. It’s based on the UserDetails interface which is meant to store user information and ultimately used by Authentication instances. For an example see the implementation of changePassword in the previous section.
As we’ll see in a moment, our implementation will delegate a number of the methods to the actual User since we currently store everything there. If you chose to store information somewhere else, your implementation of UserDetails could delegate to wherever the live. You’ll see from the methods the interface defines there is support for features like checking if an account is non-expired or non-locked. While we could record that data in our User class we might instead have our UserDetails implementation check for the last time the user made a payment via a service call or look up in the database. For now, we’ll just hard code these methods to return true.
public class CustomUserDetails implements UserDetails { private User user; public CustomUserDetails(User user) { this.user = user; } public User getUser() { return user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { String roles = StringUtils.collectionToCommaDelimitedString( user.getRoles().stream().map(Role::getRole).collect(Collectors.toSet())); return AuthorityUtils.commaSeparatedStringToAuthorityList(roles); } @Override public String getUsername() { return user.getUsername(); } @Override public String getPassword() { return user.getPassword(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return user.isEnabled(); } }
At this point we now have the basics necessary for managing users and roles. Next stop, account verification.