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:
If you want to style input fields when the contents are invalid, you
can use the :invalid and
:required
pseudo-classes.
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:
Use a POST form to collect the user's account information.
Create a salted hash for the user's password.
Save the valid user information to database tables.
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):
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.
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:
Create an SQL query string to insert into users the email, encrypted
password, and the value true for the
enabled field.
Use a MapSqlParameterSource to fill in parameters for
email and the encrypted password.
Run the query with jdbc.update() and be sure to
pass in the parameter source.
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:
Create an SQL query string to insert into user_role the provided
user ID and role ID.
Use a MapSqlParameterSource to fill in parameters for the user ID
and role ID.
Run the query with jdbc.update() and be sure to pass in the parameter
source.
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:
Invoke the database access method addUser() to insert the
new user with their email and password.
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!
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!
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:
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?
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.
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:
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.
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.
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.
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.
At this point, Spring realizes that it was already
in the process of instantiating a DatabaseAccess class
when it was creating the MainController instance.
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!"
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:
redirect:/ will redirect to the root and will trigger
the handler method mapped to "/"
redirect:/secure will redirect to the root of the
secure directory (this will trigger the handler method
mapped to "/secure")
redirect:/foo?name=bar will redirect to /foo and include
a query string with the parameter name, which is assigned
the value "bar"
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!