Using @RequestScope With Your API
A Zone Leader talks about how using Spring's @RequestScope allowed objects required at login to be re-used through the life of the request.
Join the DZone community and get the full member experience.
Join For FreeAs a part of creating a RESTful API for a conversion project, the existing security in place leveraged a deep hierarchy in the object containing users accessing the API via an Angular client.
You may also like:
To give an example, consider the following JSON example for the User class:
x
{
"id":10001,
"username":"john.doe",
"firstName":"John",
"middleName":"Danger",
"lastName":"Doe",
"role":{
"id":101,
"name":"Freelance Writer"
},
"accounts":[
{
"id":90001,
"name":"Publication One"
},
{
"id":90002,
"name":"Publication Two"
},
{
"id":90003,
"name":"Publication Three"
}
]
}
In this example, the freelance writer John "Danger" Doe is linked to three accounts, which appear as child records to the User class. In reality, the User, Role and Account objects are far more populated.
Not seen is a ticketId, which is part of a custom-built (and out of scope) authentication mechanism to keep track of end-user activity, which can expire if unused for a period of time. Of course, logging out will invalidate the ticketId too.
When a RESTful request arrives into the application, the ticketId is used to look up the User id tied to the ticket. In this case, when the ticketId is linked to User id = 10001, the john.doe record will be found.
Additionally, a path parameter for the HTTP request will include the Account id for the request that is being made. So, once the User record is returned, a check can be made to see if the accounts list includes the Account id in the request.
If everything checks out, the request is able to move to the next level — which can check to see if freelance writers do indeed have the necessary clearance to perform the request.
Original Implementation
The original implementation of the REST API passed the ticketId and the Account id from the Controller class to the Service implementation being employed. From there, a service call was made similar to what is displayed below:
accountService.getAccountById(Long accountId, String ticketId);
If the ticketId was not valid or the User did not have access to the Account linked to the accountId, exceptions would be thrown back to the Controller, which would result in a 4xx HTTP Response.
While this approach seemed to work, it had some drawbacks:
- Every service class needed to inject the accountService.
- Every service method needed to perform the security check.
- The ticketId from the request was getting passed into the service layers.
- The service was getting involved in throwing exceptions that the controller, an interceptor or a filter could be handling.
Enter Security Interceptor + @RequestScope
Since the RESTful API was using Spring Boot, a bean was created called UserData which contained the following information:
x
public class UserData {
private UserDto user;
private AccountDto account;
}
In order to keep the payload light, the User object was converted to use the UserDto class, which did not include the deep hierarchy of information. Part of the exception process would catch cases where the user did not have access to the accountId provided. So instead, the AccountDto class was being used as well, to avoid including accounts that had nothing to do with the current request.
With the UserData object created, the next step was to update the WebConfig to register the request scope bean:
xxxxxxxxxx
public UserData requestScopeUserData() {
return new UserData();
}
Next, the SecurityInterceptor class was created, which included a reference to the UserData object:
x
public class SecurityInterceptor extends HandlerInterceptorAdapter {
name = "requestScopeUserData") (
private UserData userData;
}
The preHandle() implementation would handle all of the necessary security checks and performing the conversion from User to UserDto and creating the AccountDto object as well. Once these were created, they were added to the UserData instance using a couple of setters:
x
userData.setUser(userService.getUserFromTicketId(ticketId));
userData.setAccount(accountService.getAccountById(accountId, ticketId));
In the WebConfig, the SecurityInterceptor was setup as well:
xxxxxxxxxx
public SecurityInterceptor getSecurityInterceptor() {
return new SecurityInterceptor();
}
public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getSecurityInterceptor()).addPathPatterns("/**");
}
At this point, the necessary security checks have been obscured from the service layer and processed in the background with every request being made to the API.
New Implementation Example
With everything in place, a simple controller would appear as shown below:
x
name = "Invoice API") (
value = "/accounts", produces = MediaType.APPLICATION_JSON_VALUE) (
public class InvoiceController {
private final InvoiceService invoiceService;
summary = "For a given accountId, return a List of invoices.") (
value = { (responseCode = Constants.OK, description = Constants.OK_TEXT), (
responseCode = Constants.UNAUTHORIZED, description = "User from ticketId does not have access or ticketId is expired", content = ()), (
responseCode = Constants.FORBIDDEN, description = "User does not have access to underlying Account", content = ()) }) (
value = "{accountId}/invoices") (
HttpStatus.OK) (
public ResponseEntity<List<InvoiceDto>> getInvoices( Long accountId, String ticketId) {
try {
return new ResponseEntity<>(invoiceService.getInvoicesByAccountId(accountId, ticketId), HttpStatus.OK);
} catch (SomeException e) {
return new ResponseEntity<>(ExceptionUtils.handleUserExceptions(e));
}
}
}
If an exception is thrown by the SecurityInterceptor, it will be reflected in the HTTP Response for the API and not go any further. If the request does not encounter an exception in the SecurityInterceptor, the service class would be called:
x
public class InvoiceServiceImpl implements InvoiceService {
name = "requestScopeUserData") (
private UserData userData;
private final Converter converter;
private final InvoiceRepository invoiceRepository;
/**
* {@inheritDoc}
*/
public List<InvoiceDto> getInvoicesByAccountId(Long accountId, String ticketId) throws SomeException {
SecurityUtils.hasEditorOrAboveAccess(userData.getUser());
return converter.convert(invoiceRepository.getInvoicesByAccountId(accountId));
}
}
As shown above, the UserData set in the SecurityInterceptor is available for use — freeing the service layer from having to make that same request again. This is good news since the UserDto in the UserData object is required to call the static SecurityUtils class in order to determine if the user has proper authority to receive the list of invoices.
Source Code Link
A simple example of the approaches employed for the article can be found in the following link at GitLab:
https://gitlab.com/johnjvester/request-scope
Have a really great day!
Further Reading
Opinions expressed by DZone contributors are their own.
Comments