Creating a User Model
The next few sections walk through the steps necessary to enable simple authentication in the codingevents
app. Along the way, we will use some advanced concepts that you haven’t fully learned. That’s okay. We don’t expect you to understand every detail the next few sections. However, you do need to understand the purpose of each step in enabling authentication.
While we’ll use codingevents
, these steps would be the same for any other app. If you want to add simple authentication to a Spring Boot application in the future, reference this chapter.
Before we can authenticate users, we need users to authenticate! We’ll start by adding a User
model.
A User
Model
You can use the add-tags branch as your starting point for this section. Be sure to create a new branch as you begin your work.
A model class representing users needs, at a minimum, fields representing username and password.
In the models
package, create a User
class with @Entity
and extending AbstractEntity
. It should have two string fields, username
and pwHash
. We only need a getter for username
.
|
|
Notice that the constructor takes a parameter named password
and uses it to set the value of pwHash
. We mentioned previously
that we should never store passwords, so in a moment, we will update line 26 by creating a hash from the given password to store.
Our validation annotations on User
are very lenient. This is okay, however, because we will validate user input used to make User
objects using a DTO with more restrictive validation.
Hashing Passwords
We’ll use the bycrypt hash algorithm. One way to access this algorithm is via the following dependency:
implementation("org.springframework.security:spring-security-crypto:5.5.1")
Add this as an implementation
dependency in your build.gradle
file.
This dependency provides the BCryptPasswordEncoder
class, which we will use to create and verify hashes. While our class needs one of these encoder objects, it does not need to be an instance variable. We’ll make it static so it can be shared by all User
objects.
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
Add the above line of code to your User
class.
In the constructor, we can use encoder
to create a hash from the given password:
public User(String username, String password) {
this.username = username;
this.pwHash = encoder.encode(password);
}
Our User
objects should also be responsible for determining if a given password is a match for the hash stored by the object. We can do this using the encoder.matches()
method. Let’s put this behavior in a method at the bottom of our User
class:
public boolean isMatchingPassword(String password) {
return encoder.matches(password, pwHash);
}
Notice that we are using encoder.matches()
rather than directly comparing hash values. More explicitly, the following comparison will NOT work to compare hashes generated by bcrypt:
public boolean isMatchingPassword(String password) {
String candidateHash = encoder.encode(password);
return candidateHash.equals(pwHash);
}
While our conceptual example in the previous section used direct comparison of hashes—and some hashing techniques allow you to do so—bcrypt does not. This is because bcrypt internally uses a technique called salting
, which requires additional steps before comparison. These additional steps are carried out by encoder.matches()
.
Creating the UserRepository
As usual, we need a repository in order to access User
objects stored in the database. This time, however, we add a twist. Create UserRepository
in the data
package, with the following contents:
public interface UserRepository extends CrudRepository<User, Integer> {
User findByUsername(String username);
}
While our repository extends CrudRepository
, it also contains a new method, findByUsername
. Based on the method signature, it appears that this method is intended to take a username, and return the given user with that username. Indeed, when our application runs, the UserRepository
will have such a method.
Spring allows for additional, custom methods to be added to repository interfaces, as long as they follow some basic naming conventions. These conventions are straightforward to use, and allow you to create additional, more sophisticated query methods. Methods created in this way are called query methods, and their rules are defined in the Spring documentation .
The final code for this section is available in the user-model branch
of the CodingEventsJava
repository.
Check Your Understanding
Why can we not use Java string comparison when evaluating values generated with bcrypt?
- bcrypt will never create two matching hashes.
- Java does not have a native string comparator method.
- Salting adds variance to hashes generated from the same plain text.
- Answers a and c.
public interface UserRepository extends CrudRepository<User, Integer> {
User findByUsername(String username);
}
True/False: From the code block above, the line User findByUsername(String username);
is missing a return statement.
- True
- False