User
Model¶The next few sections walk through the steps necessary to enable simple authentication in the coding-events
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.
Note
While we’ll use coding-events
, 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.
User
Model¶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
.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @Entity
public class User extends AbstractEntity {
@NotNull
private String username;
@NotNull
private String pwHash;
public User() {}
public User(String username, String password) {
this.username = username;
this.pwHash = password;
}
public String getUsername() {
return 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.
Note
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.
We’ll use the bycrypt hash algorithm. One way to access this algorithm is via the following dependency:
org.springframework.security:spring-security-crypto
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.
25 | private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
In the constructor, we can use encoder
to create a hash from the given password:
29 30 31 32 | 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:
38 39 40 | public boolean isMatchingPassword(String password) {
return encoder.matches(password, pwHash);
}
|
Warning
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()
.
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:
9 10 11 12 13 | 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 Spring’s documentation.
Note
The code for this section is available in the
user-model branch
of the coding-events-demo
repository.
Question
Why can we not use Java string comparison when evaluating values generated with bcrypt?
Question
9 10 11 12 13 | public interface UserRepository extends CrudRepository<User, Integer> {
User findByUsername(String username);
}
|
True/False: From the code block above, line 11 is missing a return statement.