Setting up Spring security
Let us start with a very simple application. Using your IDE, create a Spring boot application with the following dependencies: Web, JPA and H2 (let us just keep it simple). To start with our experience, let us add the very first controller we used in our course:
That is it, you can go to your browser an test the application (e.g. go to the web page http://localhost:8080/sayHello).
Now, add the following maven dependency to your project’s pom.xml
file:
If you restart your application and go again to http://localhost:8080/sayHello you will notice that you are not longer authorized. In fact, the application will redirect you to a login page, where you are expected to enter your credentials.
If you check the application logs, you will see that it printed out a message similar to the one below:
Using generated security password: 478c81b2-257e-4c2c-84f4-fb6c39555ff0
When Spring boot runtime detects the jars associated with spring-boot-starter-security
,
it will change the application to add a basic infrastructure to secure the access
to the application’s web pages. No additional changes would be required if we
agree with the default settings. Unfortunately, the default configuration is
such that we have one single user user
with a randomly generated password,
e.g. the one that is dumped into the logs. Try out again the application,
now that you know the credentials. Definitively, we would need to change the
default configuration.
In fact, you can set the password for the default user on the application.properties
file. For instance, you can add the following into such file:
and, when you restart your application, you will be able to use password
for logging-in.
You will also notice that the above overrides the configuration such that no random
password is generated anymore.
However, the approach is not robust and is something you would not certainly use in production.
Therefore, I will ask you first to remove the line setting up the password inside the
application.properties
file. Then, we will learn how to setup a more appropriate
authentication service with Spring security.
Adding in memory authentication
Firstly, we will setup an authentication service that keeps all the information for authentication in main memory. To this end, add a configuration class (in a separate file, if you wish) with the following code.
I believe that the code above is self-descriptive but, just in case, it registers two users into
the authentication service. You will notice that the method withDefaultPasswordEncoded
is marked
as deprecated. Do not be worried about that, it will work anyway. In fact, we are using such default
encoder because we are hard coding the actual password and expecting that such passwords are encoded
by the default encoder. Hard coding the passwords is a bad practice, and that is the reason why
the method withDefaultPasswordEncoded
is reported as deprecated.
Configuring access rules
There are several ways to configure authorization and we will start with the one referred to as "security rules". The idea with this approach is to specify global rules, using the paths on URLs to filter the access of users. This is also accomplished with a configuration class. Although we can use separate configuration classes, I will propose to use a single class. In fact, our application is simple such that the corresponding configuration will remain short.
To avoid confusion, replace fully the code of the configuration class that we used in the previous section with the following code:
The changes are subtle but worth discussing. First, we made class SecurityConfigurator
to extend
WebSecurityConfigurerAdapter
. The latter class is the one that concentrates all the security
default settings and we will change them by overriding the method configure
. This method
receives as parameter a reference to HttpSecurity
. If you analyze carefully the code of that
method, you will identify three parts:
-
In the first part, we are disabling the protection to cross-site request forgery attack (abbreviated as CSRF). The latter attach usually relies on hijacking cookies to simulate authorized accesses. However, in our context we would be working with REST interactions and, hence, we would not be using cookies such that CSRF does not make sense (its protection relies on the use of tokens over HTTP headers, such that it is better to disable CSRF to avoid handling the tokens).
-
In the second part, we define global access rules, which I am illustrating with three examples:
-
We are authorizing the access to the home page (i.e. "/") to any user, authenticated or not,
-
We are restricting the access to the page "/sayHello" to users having the role "USER1", and
-
We are requiring that any access to paths matching the expression "/api/**" is granted only to authenticated users.
-
-
In the last part, we are opening the access to the login form to any user. Note, however, that this last part is there only for demonstration purposes: providing a login form does not make sense if we are accessing the functionality via REST interactions.
Restart the application a play with it using the browser.
Setting up HTTP Basic
Well, we are interested in adding securing the access to our REST APIs. Therefore, it does not sense to have a login page and we have to find a way to communicate the security tokens over REST interactions. In that respect, we can find several possible approaches to this problem including JSON web tokens (JWS) and others. To keep it simple, I decided to use a simpler approach, i.e. HTTP Basic, because we would be using several types of technologies and using JWS would require lot of configuration for each one of them.
Well, let us first update the security configuration class. To that end, replace the method
configure
on class SecurityConfigurator
with the following code:
As you can see, we have removed most of the lines that were intended to facilitate the demonstration
and added a new line with a call to httpBasic()
. The latter is the one that adds a spring component
(a filter), that intercepts the REST interactions and tries to get the authentication token from the
HTTP headers.
In the client side, the HTTP basic access authentication protocol implies the following:
-
The username and password are concatenated with a colon in between them to generate a single string, preferably using UTF-8,
-
The resulting string in the previous step is then encoded using a variant of Base64, and
-
The encoded string is included in every HTTP request as the authorization header as:
Authorization: Basic dXNlcjE6dXNlcjE=
, wheredXNlcjE6dXNlcjE=
is the encoded security token.
To test the application, I propose you to use Postman. Configure a GET interaction over
http://localhost:8080/sayHello . You will notice that Postman provide a tab "Authentication" where,
after selecting Basic Auth
, entering "user1" both as username and password, and selecting
Update Request
, you will see that a Header will be added with the corresponding information.
So, let us give a try. If everything is configured as specified here, you should be able to
see "Hello world" in the response body shown by Postman. If something is wrong check that you
restarted the application and entered the correct credentials. BTW, it would interesting to
try with the other user or with wrong credentials to see the result.
Method level authorization
We will now introduce another approach to specifying the authorization, where the control happens at the level of the method. It is worth mentioning that this approach is very similar to the one used by Java enterprise edition (JEE), so you might come across with a piece of code that looks similar to ours.
Let us start by adding the annotation @EnableGlobalMethodSecurity(securedEnabled=true)
to our
SecurityConfigurator
class. In fact, you can add it to any class that carries the annotation
@Configuration
or to the project’s entry class.
As way of example, copy the following code there:
On the code above, we are defining two REST endpoints and we are assuming that the application
recognizes three possible roles: "ADMIN", "USER1" and "USER2". Now, the annotation @Secured
allows you to specify the roles to which you grant permission to access the underlying method.
Note that the roles are specified with the prefix ROLE_
. However, role ROLE_USER1
corresponds with USER1
in the configuration, so on and so forth. With this information you
would straightforwardly understand that getResource1()
will only be accessible to user user1
and that getResource2()
will only be accessible to user user2
, according to our current
configuration.
Please also notice that in the first method, I intentionally added one line that allows us to
get access to the user information from the SecurityContextHolder
. This approach could be
used to determine who is the issuer of a request (e.g. the name of the site engineer that
is creating the plant hire request).
Configuring Cross-Origin Request Sharing (CORS)
Since our REST API is to be access from a Javascript frontend application, we would need to
configure CORS. In fact, we already know that we can use the annotation @CrossOrigin
on
every single rest controller we want to make available to the frontend application. However,
we also need to adapt the configuration of Spring security, as shown below.
As you can see, the changes are simple but deserve a few words of explanation. First, the
calls .and().cors()
add the components to support the mechanism (also in the form of
a filter). On the other hand, the sequence of calls
.antMatchers(HttpMethod.OPTIONS, "/api/").permitAll()
is intended to allow the interactions
with the backend to complete interactions required by CORS (the so-called preflight** which
relies on a request with HTTP OPTIONS).
Configuring a JDBC-based authentication service
Adding a JDBC-based authentication service to our application is easy with Spring security.
In fact, we can use the same database that we use for storing the application domain classes
even if the latter are accessed with JPA repositories. To this end, we need basically two
things. First, we need to get access to the JDBC data source, which can be done with an
autowired annotation as shown in the code below. Second, we need to configure the authentication
service. The latter can be done with the sequence of calls: .jdbcAuthentication().dataSource(dataSource).withDefaultSchema()
.
Note that we are passing the reference to the data source that was injected by the runtime and that we
are asking the runtime to use the default database schema. That means, we are allowing spring runtime to
setup the authentication database and, as we are using the embedded H2, it will be reset every time
we restart the application. Of course, you can configure this approach to use a different database
and, why not, to preserve the database content.
Please note that I added a couple of configuration lines to open the access to H2’s console, i.e.
.antMatchers("/h2-console/**").permitAll()
and http.headers().frameOptions().disable();
. You
can safely remove them from the configuration class if you are not interested in getting access
to H2’s console.