Overview of This Lesson

Refresh this page because I am probably still making changes to it.

We round off our discussion of Spring security with a demonstration of how you might design a user registration system. This is just an example of how you might implement the ability for users to register and log into your application - it will give you some exposure to classes and methods and how you might use them in other, similar applications.

Pre-requisites

Before doing this lesson, it's important that you've already gone through the following lessons:

Resources

Creating a Registration Form

In the previous lessons we allowed a user to log into our application with a customized login form. We stored user credentials inside a set of database tables. In a real application, you might have several users. In some environments, user accounts are created by the system or an administrator when employees are hired or students are enrolled. But in some applications, users can create their own accounts. You've probably created an account on at least one site yourself: if you use social media for example, or shop online at a particular online store, you would have registered your own account that you then use to log into that site whenever you want.

We can easily add the ability for users to create their own accounts by adding user registration functionality to our applications. There are several ways you can do this, but we only have time to look at a basic one. You can do a bit of exploring and modify or add to this example to do things differently if you want. In this lesson we'll add the following functions:

The form is pretty easy: here's an example:

<form name="form" action="#" th:action="@{/register}" method="post">
<p><label for="email">Email: <input type="email" name="email" id="email" 
  title="Enter a valid email address." required></label>
</p>
<p><label for="pass">Password: <input type="password" name="pass" id="pass" 
  title="Choose a password." required></label>
</p>
<p><label for="verify">Verify Password: <input type="password" name="verify" 
id="verify" title="Re-enter the same password." required></label>
</p>
<p><input type="submit" value="Register"></p>
</form>

Client-Side Validation

It's always important to perform client-side validation with form inputs. In the past we've been able to get away with HTML5 constraint validation but in this case, we need some JavaScript to ensure that the user-entered password value matches the user-entered "verify password" value. I have an old script that does this (along with a few other things) - it's not the greatest but it will work for now if you don't have time to write something better: Quick Client Side Registration Form Validation. Add it to your /static/scripts or /static/js directory, whichever you normally use (make sure it matches the ant-matcher in your Security Config), and add the <script> element to your register.html page (don't forget to use th:src="@{/scripts/filename.js}").

Handler and Ant Matchers for Registration Form

When you add the registration form, you'll need to add a Controller handler method to load the form, and you'll also need to add that pattern to the list of patterns in SecurityConfig's configure() method so that it's accessible without being authenticated (I just added /registration to my list of allowed antMatchers):

.antMatchers("/", "/js/**", "/css/**", "/images/**", "/register").permitAll()

Adding DatabaseAccess Methods

When the user submits the form with valid data, we can call upon a database method that inserts a new record into the users table. However, in addition to adding the user, we need to also add an entry to user_role to record that this user belongs to a specific role. For this example, I'll assign new users to the GUEST role by default.

When we insert a new user into the users table, we'll need to create a password hash for the user-entered password. So in the database access class, we'll need a BCryptPasswordEncoder instance. Recall in the previous lesson we added a @Bean method to our SecurityConfig that creates a new BCryptPasswordEncoder and adds it to the inversion of control container. We can autowire that instance in our DatabaseAccess class and then use it when we insert the user record with the password hash.

@Autowired
private BCryptPasswordEncoder passwordEncoder;

Now we can add an addUser() method: it will need the email address and password the user input via the form:

public int addUser(String email, String password) {

}

The rest is pretty straight-foward:

  1. Create an SQL query string to insert into users the email, encrypted password, and the value true for the enabled field.
  2. Use a MapSqlParameterSource to fill in parameters for email and the encrypted password.
  3. Run the query with jdbc.update() and be sure to pass in the parameter source.
contact wendi if you use a screen reader
The addUser() method

We also need a method that adds an entry to user_role. This method needs the user ID and the role ID for the role you want this user to belong to:

public void addUserRole(long userId, long userRole) {

}

This method is also pretty basic:

  1. Create an SQL query string to insert into user_role the provided user ID and role ID.
  2. Use a MapSqlParameterSource to fill in parameters for the user ID and role ID.
  3. Run the query with jdbc.update() and be sure to pass in the parameter source.
contact wendi if you use a screen reader
The addUserRole() method

Updating the Controller

Now that we have the database access methods, we can add the controller handler method to process the form data. I used the same pattern for loading the registration form, but since the form uses the POST method, I used a PostMapping. I also included request parameters for the email and password (we don't need the verify-password value on the server-side):

@GetMapping("/register")
public String loadRegForm() {
    return "register.html";
}

@PostMapping("/register")
public String processRegForm(@RequestParam String email, @RequestParam String pass) {

}

You'll also need to autowire the database access class in your controller so you can call the database access methods!

Now we can complete the handler method that registers the new user:

  1. Invoke the database access method addUser() to insert the new user with their email and password.
  2. We need to now invoke the addUserRole() but we need the user ID to do that:
    • We don't have a User ID until we've inserted the new user (remember, it's an auto-increment primary key field).
    • Retrieve the user we just added: we wrote a method for this in the previous lesson: findUserAccount(email).
    • Once you have the user object, you can pass that user's ID to the addUserRole() method!
  3. Once you've inserted the user and user_role records, we need to load a view. Which page? It's up to you, really. The user will probably want to log in once they've created their account, so the login page might be a good choice!
contact wendi if you use a screen reader
The controller handler method

The Circular Dependency Problem

At this point you can test out your program. But first, make sure that you've un-commented the code that unblocks the h2-console so that we can check to see if the new user was successfully added. Make those changes and save everything.

Now start your program like you normally would. What happens?

You'll notice an error in the console with text similar to this:

Exception encountered during context initialization
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating 
bean with name 'mainController': Unsatisfied dependency expressed through field 
'da'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'databaseAccess': Unsatisfied dependency expressed through 
field 'encoder'; nested exception is 
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'securityConfig': Unsatisfied dependency expressed through field 
'userDetailsService'; nested exception is 
org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'userDetailsServiceImpl': Unsatisfied dependency
expressed through field 'da'; nested exception is 
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating
 bean with name 'databaseAccess': Requested bean is currently in creation: Is 
 there an unresolvable circular reference?

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   mainController (field private ca.sheridancollege.jollymor.sec.database.DatabaseAccess ca.sheridancollege.jollymor.sec.controllers.MainController.da)
┌─────┐
|  databaseAccess (field private org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder ca.sheridancollege.jollymor.sec.database.DatabaseAccess.encoder)
↑     ↓
|  securityConfig (field private ca.sheridancollege.jollymor.sec.services.UserDetailsServiceImpl ca.sheridancollege.jollymor.sec.security.SecurityConfig.userDetailsService)
↑     ↓
|  userDetailsServiceImpl (field private ca.sheridancollege.jollymor.sec.database.DatabaseAccess ca.sheridancollege.jollymor.sec.services.UserDetailsServiceImpl.da)
└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default. 
Update your application to remove the dependency cycle between beans. As a last resort, 
it may be possible to break the cycle automatically by setting 
spring.main.allow-circular-references to true.

The error message says that the database access bean can't be created in the controller because it has been injected into the UserDetailsServiceImpl class. It's creating a circular reference. What does this mean?

  1. When the Spring application starts, it looks for @Beans and @Components (or types of @Component, such as @Repository, @Service, and @Controller components) and instantiates them and stores them in the Inversion of Control container.
  2. This means it needs to instantiate and store the DatabaseAccess instance, UserDetailsServiceImpl instance, MainController instance, SecurityConfig instance, and several other objects. Before it starts, it tries to lay out the whole process and make sure that there are no errors. Spring imagines how this will work:
    1. When Spring initializes MainController, it sees that it has a reference to the DatabaseAccess class, so it constructs DatabaseAccess so that it can be injected into the MainController.
    2. As it's constructing DatabaseAccess, it sees that it has a reference to BCryptPasswordEncoder, which is a @Bean method in SecurityConfig. So Spring has to go instantiate SecurityConfig first so it can grab the BCryptPasswordEncoder.
    3. As Spring instantiates SecurityConfig, it sees that it has a reference to UserDetailsServiceImpl, so Spring has to go and find that and instantiate that so it can inject it into SecurityConfig.
    4. As Spring instantiates UserDetailsServiceImpl, it sees that it has a reference to DatabaseAccess, so it has to go and instantiate DatabaseAccess so that it can inject it.
    5. At this point, Spring realizes that it was already in the process of instantiating a DatabaseAccess class when it was creating the MainController instance.
  3. This is the problem that Spring sees when it's starting up your program, and that's what the error is referring to: It's saying, "I had a problem in the MainController when I was creating the DatabaseAccess instance, because I ended up in a kind of endless loop when I got to the UserDetailsServiceImpl instance!"
  4. This "endless loop" is actually called a circular dependency: A circular dependency occurs when objects are dependent upon other objects in a circular way e.g. ClassA needs ClassB which needs ClassC which needs ClassA.

We can fix this circular dependency issue by using the annotation @Lazy when we @Autowired the DatabaseAccess class in both MainController and UserDetailsServiceImpl.

The @Lazy annotation indicates that a component that is @Autowired in more than one place should only be created and injected the first time it's needed. This will avoid the "circular reference" problem. When Spring encounters the @Autowired @Lazy with the DatabaseAccess instance, it makes note that it's there but doesn't actually inject it. It waits to inject the DatabaseAccess class until it's actually called upon. That way, there won't be a circular reference when all the different components are being instanitated and injected on program startup.

Add the @Lazy attribute to the DatabaseAccess declaration in both the MainController AND the UserDetailsServiceImpl:

@Autowired @Lazy
private DatabaseAccess da;

Or

@Autowired @Lazy private DatabaseAccess da;

is fine too.

Redirects

Now you can re-run your program and test it. Run the application and then browse to the Registration page. Add a new user and then log in as the new user. But I want you to note one thing: What's the URL on the login page after you register?

If you look at the address bar, you'll see http://localhost:8080/register. That doesn't make much sense - the user isn't registering anymore, they already registered. If the user decides to bookmark this page for quick and easy access, it could cause problems. You might have actually noticed this already in several of your programs: often when a form's handler method executes, it leaves us with a URL that doesn't make much sense.

Well I wouldn't mention it if we didn't have a fix for it, right? Try changing the handler method's return statement to:

return "redirect:/login";

A redirect simply redirects the user to another URL (or URL pattern, in this case). This will redirect the request to /login, which will invoke the /login handler method, and that's fine: that will load the login.html page but it will also make the URL say http://localhost:8080/login. This makes more sense and can be bookmarked without any issues.

The syntax of the redirect is just the string "redirect:" followed by the path or pattern. Examples:

Feel free to further test your application by visiting the different pages with your new user and adding other users. If you want to test the USER role, just edit the handler method to use the role ID for your USER role instead.

You probably had some concerns about how secure it is to transmit the raw user-entered password to the server before its hashed. Those concerns are definitely valid: you should never use form authentication unless you're using a secure HTTP connection. But how can we test this out in our applications in a development environment? We'll find out in the next lesson!

Exercises

In the last lesson you added database form authentication to at least one of your other projects. Add a user registration component to that project(s). Feel free to customize the functionality!