In the previous section, you made the app dynamically response to updates from other users with Spring Data REST’s built-in event handlers and the Spring Framework’s WebSocket support. However, no application is complete without securing the whole thing so that only proper users have access to the UI and the resources behind it.
Feel free to grab the code from this repository and follow along. This section is based on the previous section’s app with extra things added.
Before getting underway, you need to add a couple dependencies to your project’s pom.xml file:
link:pom.xml[role=include]
This bring in Spring Boot’s Spring Security starter as well as some extra Thymeleaf tags to do security lookups in the web page.
In the past section, you have worked with a nice payroll system. It is handy to declare things on the backend and let Spring Data REST do the heavy lifting. The next step is to model a system where security controls need to be instituted.
If this is a payroll system, then only managers would be accessing it. So kick things off by modeling a Manager
object:
link:src/main/java/com/greglturnquist/payroll/Manager.java[role=include]
-
PASSWORD_ENCODER
is the means to encrypt new passwords or to take password inputs and encrypt them before comparison. -
id
,name
,password
, androles
define the parameters needed to restrict access. -
The customized
setPassword()
method ensures that passwords are never stored in the clear.
There is a key thing to keep in mind when designing your security layer. Secure the right bits of data (like passwords) and do NOT let them get printed to console, into logs, or exported through JSON serialization.
-
@JsonIgnore
applied to the password field protects from Jackson serializing this field.
Spring Data is so good at managing entities. Why not create a repository to handle these managers? The following code does so:
link:src/main/java/com/greglturnquist/payroll/ManagerRepository.java[role=include]
Instead of extending the usual CrudRepository
, you do not need so many methods. Instead, you need to save data (which is also used for updates), and you need to look up existing users. Hence, you can use Spring Data Common’s minimal Repository
marker interface. It comes with no predefined operations.
Spring Data REST, by default, will export any repository it finds. You do NOT want this repository exposed for REST operations! Apply the @RepositoryRestResource(exported = false)
annotation to block it from export. This prevents the repository and its metadata from being served up.
The last bit of modeling security is to associate employees with a manager. In this domain, an employee can have one manager while a manager can have multiple employees. The following code defines that relationship:
link:src/main/java/com/greglturnquist/payroll/Employee.java[role=include]
-
The manager attribute is linked by JPA’s
@ManyToOne
attribute.Manager
does not need the@OneToMany
, because you have not defined the need to look that up. -
The utility constructor call is updated to support initialization.
Spring Security supports a multitude of options when it comes to defining security policies. In this section, you want to restrict things such that ONLY managers can view employee payroll data, and that saving, updating, and deleting operations are confined to the employee’s manager. In other words, any manager can log in and view the data, but only a given employee’s manager can make any changes. The following code achieves these goals:
link:src/main/java/com/greglturnquist/payroll/EmployeeRepository.java[role=include]
-
@PreAuthorize
at the top of the interface restricts access to people withROLE_MANAGER
.
On save()
, either the employee’s manager is null (initial creation of a new employee when no manager has been assigned), or the employee’s manager’s name matches the currently authenticated user’s name. Here, you are using Spring Security’s SpEL expressions to define access. It comes with a handy ?.
property navigator to handle null checks. It is also important to note using the @Param(…)
on the arguments to link HTTP operations with the methods.
On delete()
, the method either has access to the employee, or if it has only an id
, it must find the employeeRepository
in the application context, perform a findOne(id)
, and check the manager against the currently authenticated user.
A common point of integration with security is to define a UserDetailsService
. This is the way to connect your user’s data store into a Spring Security interface. Spring Security needs a way to look up users for security checks, and this is the bridge. Thankfully, with Spring Data, the effort is quite minimal:
link:src/main/java/com/greglturnquist/payroll/SpringDataJpaUserDetailsService.java[role=include]
SpringDataJpaUserDetailsService
implements Spring Security’s UserDetailsService
. The interface has one method: loadUserByUsername()
. This method is meant to return a UserDetails
object so that Spring Security can interrogate the user’s information.
Because you have a ManagerRepository
, there is no need to write any SQL or JPA expressions to fetch this needed data. In this class, it is autowired by constructor injection.
loadUserByUsername()
taps into the custom finder you wrote a moment ago, findByName()
. It then populates a Spring Security User
instance, which implements the UserDetails
interface. You are also using Spring Securiy’s AuthorityUtils
to transition from an array of string-based roles into a Java List
of type GrantedAuthority
.
The @PreAuthorize
expressions applied to your repository are access rules. These rules are for nought without a security policy:
link:src/main/java/com/greglturnquist/payroll/SecurityConfiguration.java[role=include]
This code has a lot of complexity in it, so we will walk through it, first talking about the annotations and APIs. Then we will discuss the security policy it defines.
-
@EnableWebSecurity
tells Spring Boot to drop its autoconfigured security policy and use this one instead. For quick demos, autoconfigured security is okay. But for anything real, you should write the policy yourself. -
@EnableGlobalMethodSecurity
turns on method-level security with Spring Security’s sophisticated@Pre
and@Post
annotations. -
It extends
WebSecurityConfigurerAdapter
, a handy base class for writing policy. -
It autowires the
SpringDataJpaUserDetailsService
by field injection and then plugs it in through theconfigure(AuthenticationManagerBuilder)
method. ThePASSWORD_ENCODER
fromManager
is also set up. -
The pivotal security policy is written in pure Java with the
configure(HttpSecurity)
method call.
The security policy says to authorize all requests by using the access rules defined earlier:
-
The paths listed in
antMatchers()
are granted unconditional access, since there is no reason to block static web resources. -
Anything that does not match that policy falls into
anyRequest().authenticated()
, meaning it requires authentication. -
With those access rules set up, Spring Security is told to use form-based authentication (defaulting to
/
upon success) and to grant access to the login page. -
BASIC login is also configured with CSRF disabled. This is mostly for demonstrations and not recommended for production systems without careful analysis.
-
Logout is configured to take the user to
/
.
Warning
|
BASIC authentication is handy when you are experimenting with curl. Using curl to access a form-based system is daunting. It is important to recognize that authenticating with any mechanism over HTTP (not HTTPS) puts you at risk of credentials being sniffed over the wire. CSRF is a good protocol to leave intact. It is disabled to make interaction with BASIC and curl easier. In production, it is best to leave it on. |
One part of a good user experience is when the application can automatically apply context. In this example, if a logged-in manager creates a new employee record, it makes sense for that manager to own it. With Spring Data REST’s event handlers, there is no need for the user to explicitly link it. It also ensures the user does not accidentally assign records to the wrong manager. The SpringDataRestEventHandler
handles that for us:
link:src/main/java/com/greglturnquist/payroll/SpringDataRestEventHandler.java[role=include]
-
@RepositoryEventHandler(Employee.class)
flags this event handler as applying only toEmployee
objects. The@HandleBeforeCreate
annotation gives you a chance to alter the incomingEmployee
record before it gets written to the database.
In this situation, you can look up the current user’s security context to get the user’s name. Then you can look up the associated manager by using findByName()
and apply it to the manager. There is a little extra glue code to create a new manager if that person does not exist in the system yet. However, that is mostly to support initialization of the database. In a real production system, that code should be removed and instead depend on the DBAs or Security Ops team to properly maintain the user data store.
Loading managers and linking employees to these managers is straightforward:
link:src/main/java/com/greglturnquist/payroll/DatabaseLoader.java[role=include]
The one wrinkle is that Spring Security is active with access rules in full force when this loader runs. Thus, to save employee data, you must use Spring Security’s setAuthentication()
API to authenticate this loader with the proper name and role. At the end, the security context is cleared out.
With all these modifications in place, you can start the application (./mvnw spring-boot:run
) and check out the modifications by using the following curl (shown with its output):
$ curl -v -u greg:turnquist localhost:8080/api/employees/1 * Trying ::1... * Connected to localhost (::1) port 8080 (#0) * Server auth using Basic with user 'greg' > GET /api/employees/1 HTTP/1.1 > Host: localhost:8080 > Authorization: Basic Z3JlZzp0dXJucXVpc3Q= > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Frame-Options: DENY < Set-Cookie: JSESSIONID=E27F929C1836CC5BABBEAB78A548DF8C; Path=/; HttpOnly < ETag: "0" < Content-Type: application/hal+json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Tue, 25 Aug 2015 15:57:34 GMT < { "firstName" : "Frodo", "lastName" : "Baggins", "description" : "ring bearer", "manager" : { "name" : "greg", "roles" : [ "ROLE_MANAGER" ] }, "_links" : { "self" : { "href" : "http://localhost:8080/api/employees/1" } } }
This shows a lot more detail than you saw in the first section. First of all, Spring Security turns on several HTTP protocols to protect against various attack vectors (Pragma, Expires, X-Frame-Options, and others). You are also issuing BASIC credentials with -u greg:turnquist
which renders the Authorization header.
Amidst all the headers, you can see the ETag
header from your versioned resource.
Finally, inside the data itself, you can see a new attribute: manager
. You can see that it includes the name and roles but NOT the password. That is due to using @JsonIgnore
on that field. Because Spring Data REST did not export that repository, its values are inlined in this resource. You will put that to good use as you update the UI in the next section.
With all these modifications in the backend, you can now shift to updating things in the frontend. First of all, you can show an employee’s manager inside the <Employee />
React component:
link:src/main/js/app.js[role=include]
This merely adds a column for this.props.employee.entity.manager.name
.
If a field is shown in the data output, it is safe to assume it has an entry in the JSON Schema metadata. You can see it in the following excerpt:
{ ... "manager" : { "readOnly" : false, "$ref" : "#/descriptors/manager" }, ... }, ... "$schema" : "https://json-schema.org/draft-04/schema#" }
The manager
field is not something you want people to edit directly. Since it is inlined, it should be viewed as a read-only attribute. To filter out inlined entries from the CreateDialog
and UpdateDialog
, you can delete such entries after fetching the JSON Schema metadata in loadFromServer()
:
link:src/main/js/app.js[role=include]
This code trims out both URI relations as well as $ref entries.
With security checks configured on the backend, you can add a handler in case someone tries to update a record without authorization:
link:src/main/js/app.js[role=include]
You had code to catch an HTTP 412 error. This traps an HTTP 403 status code and provides a suitable alert.
You can do the same for delete operations:
link:src/main/js/app.js[role=include]
This is coded similarly with a tailored error message.
The last thing to crown this version of the application is to display who is logged in as well provide a logout button by including this new <div>
in the index.html
file ahead of the react
<div>
:
link:src/main/resources/templates/index.html[role=include]
To see these changes in the frontend, restart the application and navigate to http://localhost:8080.
You are immediately redirected to a login form. This form is supplied by Spring Security, though you can create your own if you wish. Log in as greg
/ turnquist
, as the following image shows:
You can see the newly added manager column. Go through a couple pages until you find employees owned by oliver, as the following image shows:
Click on Update, make some changes, and then click Update again. It should fail with the following pop-up:
If you try Delete, it should fail with a similar message. If you create a new employee, it should be assigned to you.
In this section, you:
-
Defined the model of
manager
and linked it to an employee through a 1-to-many relationship. -
Created a repository for managers and told Spring Data REST to not export.
-
Wrote a set of access rules for the employee repository and also write a security policy.
-
Wrote another Spring Data REST event handler to trap creation events before they happen so that the current user could be assigned as the employee’s manager.
-
Updated the UI to show an employee’s manager and also display error pop-ups when unauthorized actions are taken.
Issues?
The webpage has become quite sophisticated. But what about managing relationships and inlined data? The create and update dialogs are not really suited for that. It might require some custom written forms.
Managers have access to employee data. Should employees have access? If you were to add more details like phone numbers and addresses, how would you model it? How would you grant employees access to the system so they could update those specific fields? Are there more hypermedia controls that would be handy to put on the page?