Login and Registration Forms
With a User
class in place, we can now create controllers and views for creating a user and verifying their credentials.
The code for this section begins with the user-model branch
and ends with the login-reg-forms branch
of the CodingEventsJava
repository.
Creating AuthenticationController
In the controllers
package, create a new class named AuthenticationController
. Since this controller will deal with User
objects, it needs a UserRepository
instance.
|
|
Session-Handling Utilities
Before creating handler methods for rendering and processing our login and registration forms, we need some utility methods for working with sessions. Below the definition of userRepository
, let’s add the following class members:
|
|
This code allows us to store and retrieve the login status of a user in a session. More specifically, a logged-in user’s user ID will be stored in their session.
Sample session data for a logged-in user:
session_id | 81LfWG9 |
---|---|
user | 42 |
The static field userSessionKey
is the key used to store user IDs. setUserInSession
uses an HttpSession
object (part of the standard javax.servlet.http
package) to store key/value pair. getUserFromSession
looks for data with the key user
in the user’s session. If it finds one, it attempts to retrieve the corresponding User
object from the database. If no user ID is in the session, or if there is no user with the given ID, null
is returned.
The HttpSession
class handles the details of session creation and lookup for us, including generating unique session IDs and session cookies.
These utility methods will allow our handlers to manage authentication.
Form DTOs
Our login and registration forms will use DTOs to help with form rendering and processing. Furthermore, since these forms will be similar—both require a username and password—we’ll use inheritance in creating our DTOs.
The DTO for the login form needs only username
and password
fields.
Create a new package
called dto
within the models
package.
|
|
LoginFormDTO
should live in the models.dto
package.
To better understand this approach, think of a DTO associated with a form as an object that represents each of the form fields. Using a DTO to represent the data associated with a form makes form rendering and processing much easier when the form does not line up with a specific model class.
In the class above, we have a password field that will store a plain-text password. However, this does not contradict our early imperative about NOT storing passwords, since LoginFormDTO
is not a persistent class.
Our registration form will ask for a username/password pair, and then ask the user to confirm the password by typing it in again. So the associated DTO can extend LoginFormDTO
and add an additional field for password verification.
|
|
RegisterFormDTO
should also live in the models.dto
package.
The Registration Form
We are now ready to build our form handlers. Before we can authenticate a user, we must have users in the application, so we’ll build the registration form first.
To render the form within AuthenticationController
is simple:
|
|
When passing an object into the view with model.addAttribute
, specifying a label for the object is optional. If a label is not specified the class name is used, with the first letter converted to lowercase.
In the method above, model.addAttribute(new RegisterFormDTO())
will pass a RegisterFormDTO
object in with the label registerFormDTO
.
Create a new register.html
file inside of the templates
directory that uses the three DTO fields to render the form fields:
|
|
The form processing handler is more complicated. Add the following code to your AuthenticationController
class so that we can look at it, and then break it down in detail.
|
|
- Lines 1-3: Define the handler method at the route
/register
that takes a validRegisterFormDTO
object, associated errors, and aModel
. In addition, the method needs anHttpServletRequest
object. This object represents the incoming request, and will be provided by Spring. - Lines 7-10: Return the user to the form if an validation errors occur.
- Line 12: Retrieve the user with the given username from the database.
- Lines 14-19: If a user with the given username already exists, register a custom error with the
errors
object and return the user to the form. See the note on usingerrors.rejectValue
below. - Lines 21-28: Compare the two passwords submitted. If they do not match, register a custom error and return the user to the form.
- Lines 29-32: At this point, we know that a user with the given username does NOT already exist, and the rest of the form data is valid. So we create a new user object, store it in the database, and then create a new session for the user.
- Finally, redirect the user to the home page.
The Errors
class we have been using in conjunction with model binding will always contain information about errors related to validation annotations on the given model. However, it can also be used to manually generate additional errors. In the method above, we call:
errors.rejectValue("username", "username.alreadyexists",
"A user with that username already exists");
errors.rejectValue
takes three parameters:
- The field containing the error.
- A label representing the error. This allows error messages to be imported from another file. While we don’t have such a file, this parameter is required.
- A default message to use if no external error message file is available (as is the case here).
This is a good time to test your application. Start it up, navigate to /register
and try to create a user. If everything goes well, you will see a new row in the user
table of the database.
The Login Form
Rendering the login form is similar to rendering the registration form:
@GetMapping("/login")
public String displayLoginForm(Model model) {
model.addAttribute(new LoginFormDTO());
model.addAttribute("title", "Log In");
return "login";
}
The form template itself should be placed in templates/login.html
, and is
also similar to the registration template:
|
|
As usual, processing the form is more complicated. Again, we’ll break it down in detail.
|
|
- Lines 1-9: The method definition, parameters, and initial error checking are nearly identical to the registration
POST
handler above. - Line 11: Retrieves the
User
object with the given password from the database. - Lines 13-17: If no such user exists, register a custom error and return to the form.
- Line 19: Retrieves the submitted password from the form DTO.
- Lines 22-26: If the password is incorrect, register a custom error and return to the form. Password verification uses the
User.isMatchingPassword()
method, which handles the details associated with checking hashed passwords. - Line 28: At this point, we know the given user exists and that the submitted password is correct. So we create a new session for the user.
- Line 30: Finally, redirect the user to the home page.
Now you can test your login form. Upon successful form submission, you should be redirected to the home page. To verify that a session was created, open Firefox’s developer tools and navigate to the Storage pane. Select Cookies > http://localhost:8080 in the left-hand pane and you should see a cookie with the key JSESSIONID
. This is the session cookie created by the application. (You may see other cookies as well, which is okay.)
Logging Out
After the complicated processes of user registration and login, logging a user out is refreshingly simple.
@GetMapping("/logout")
public String logout(HttpServletRequest request){
request.getSession().invalidate();
return "redirect:/login";
}
To log out, we simply invalidate the session associated with the given user. This removes all data from the session, so that when the user makes a subsequent request, they will be forced to log in again.
Check Your Understanding
What is the name of the new method we have introduced on the Errors
object?
Errors.hasErrors()
Errors.errors()
Errors.isNotEmpty()
Errors.rejectValue()
Which developer tool panel can we use to verify that a user session has been started?
- Console
- Network
- Storage
- Performance