5.2.2 Customizing user authentication
In the previous chapter, you settled on using Spring Data JPA as your persistence option for all taco, ingredient, and order data. It would thus make sense to persist user data in the same way. If you do so, the data will ultimately reside in a relational database, so you could use JDBC authentication. But it’d be even better to leverage the Spring Data JPA repository used to store users.
First things first, though. Let’s create the domain object and repository interface that represents and persists user information.
DEFINING THE USER DOMAIN AND PERSISTENCE
When Taco Cloud customers register with the application, they’ll need to provide more than just a username and password. They’ll also give you their full name, address, and phone number. This information can be used for a variety of purposes, including prepopulating the order form (not to mention potential marketing opportunities).
To capture all of that information, you’ll create a User class, as follows.
Listing 5.3 Defining a user entity
package tacos;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.
SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}The first thing to notice about this User type is that it’s not the same as the User class we used when creating the in-memory user details service. This one has more details about the user that we’ll need to fulfill taco orders, including the user’s address and contact information.
You’ve also probably noticed that the User class is a bit more involved than any of the other entities defined in chapter 3. In addition to defining a handful of properties, User also implements the UserDetails interface from Spring Security.
Implementations of UserDetails will provide some essential user information to the framework, such as what authorities are granted to the user and whether the user’s account is enabled.
The getAuthorities() method should return a collection of authorities granted to the user. The various is* methods return a boolean to indicate whether the user’s account is enabled, locked, or expired.
For your User entity, the getAuthorities() method simply returns a collection indicating that all users will have been granted ROLE_USER authority. And, at least for now, Taco Cloud has no need to disable users, so all of the is* methods return true to indicate that the users are active.
With the User entity defined, you can now define the repository interface as follows:
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.User;
public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}In addition to the CRUD operations provided by extending CrudRepository, UserRepository defines a findByUsername() method that you’ll use in the user details service to look up a User by their username.
As you learned in chapter 3, Spring Data JPA automatically generates the implementation of this interface at run time. Therefore, you’re now ready to write a custom user details service that uses this repository
CREATING A USER DETAILS SERVICE
As you’ll recall, the UserDetailsService interface defines only a single loadUserByUsername() method. That means it is a functional interface and can be implemented as a lambda instead of as a full-blown implementation class. Because all we really need is for our custom UserDetailsService to delegate to the UserRepository, it can be simply declared as a bean using the following configuration method.
Listing 5.4 Defining a custom user details service bean
@Bean
public UserDetailsService userDetailsService(UserRepository userRepo) {
return username -> {
User user = userRepo.findByUsername(username);
if (user != null) return user;
throw new UsernameNotFoundException("User '" + username + "' not found");
};
}The userDetailsService() method is given a UserRepository as a parameter. To create the bean, it returns a lambda that takes a username parameter and uses it to call findByUsername() on the given UserRepository.
The loadByUsername() method has one simple rule: it must never return null. Therefore, if the call to findByUsername() returns null, the lambda will throw a UsernameNotFoundException (which is defined by Spring Security). Otherwise, the User that was found will be returned.
Now that you have a custom user details service that reads user information via a JPA repository, you just need a way to get users into the database in the first place. You need to create a registration page for Taco Cloud patrons to register with the application.
REGISTERING USERS
Although Spring Security handles many aspects of security, it really isn’t directly involved in the process of user registration, so you’re going to rely on a little bit of Spring MVC to handle that task. The RegistrationController class in the following listing presents and processes registration forms.
Listing 5.5 A user registration controller
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import tacos.data.UserRepository;
@Controller
@RequestMapping("/register")
public class RegistrationController {
private UserRepository userRepo;
private PasswordEncoder passwordEncoder;
public RegistrationController(
UserRepository userRepo, PasswordEncoder passwordEncoder) {
this.userRepo = userRepo;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
public String registerForm() {
return "registration";
}
@PostMapping
public String processRegistration(RegistrationForm form) {
userRepo.save(form.toUser(passwordEncoder));
return "redirect:/login";
}
}Like any typical Spring MVC controller, RegistrationController is annotated with @Controller to designate it as a controller and to mark it for component scanning. It’s also annotated with @RequestMapping such that it will handle requests whose path is /register.
More specifically, a GET request for /register will be handled by the registerForm() method, which simply returns a logical view name of registration. The following listing shows a Thymeleaf template that defines the registration view.
Listing 5.6 A Thymeleaf registration form view
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloud</title>
</head>
<body>
<h1>Register</h1>
<img th:src="@{/images/TacoCloud.png}"/>
<form method="POST" th:action="@{/register}" id="registerForm">
<label for="username">Username: </label>
<input type="text" name="username"/><br/>
<label for="password">Password: </label>
<input type="password" name="password"/><br/>
<label for="confirm">Confirm password: </label>
<input type="password" name="confirm"/><br/>
<label for="fullname">Full name: </label>
<input type="text" name="fullname"/><br/>
<label for="street">Street: </label>
<input type="text" name="street"/><br/>
<label for="city">City: </label>
<input type="text" name="city"/><br/>
<label for="state">State: </label>
<input type="text" name="state"/><br/>
<label for="zip">Zip: </label>
<input type="text" name="zip"/><br/>
<label for="phone">Phone: </label>
<input type="text" name="phone"/><br/>
<input type="submit" value="Register"/>
</form>
</body>
</html>When the form is submitted, the processRegistration() method handles the HTTPS POST request. The form fields will be bound to a RegistrationForm object by Spring MVC and passed into the processRegistration() method for processing. RegistrationForm is defined in the following class:
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.Data;
import tacos.User;
@Data
public class RegistrationForm {
private String username;
private String password;
private String fullname;
private String street;
private String city;
private String state;
private String zip;
private String phone;
public User toUser(PasswordEncoder passwordEncoder) {
return new User(
username, passwordEncoder.encode(password),
fullname, street, city, state, zip, phone);
}
}For the most part, RegistrationForm is just a basic Lombok class with a handful of properties. But the toUser() method uses those properties to create a new User object, which is what processRegistration() will save, using the injected UserRepository.
You’ve no doubt noticed that RegistrationController is injected with a PasswordEncoder. This is the exact same PasswordEncoder bean you declared earlier. When processing a form submission, RegistrationController passes it to the toUser() method, which uses it to encode the password before saving it to the database. In this way, the submitted password is written in an encoded form, and the user details service will be able to authenticate against that encoded password.
Now the Taco Cloud application has complete user registration and authentication support. But if you start it up at this point, you’ll notice that you can’t even get to the registration page without being prompted to log in. That’s because, by default, all requests require authentication. Let’s look at how web requests are intercepted and secured so you can fix this strange chicken-and-egg situation.
