Geek Culture
Published in

Geek Culture

Custom Secured Annotation Using Spring Expression Language & Spring AOP

Introduction

Most of us are aware of Spring Security and its usage. It supports method-level authorization using @Secured annotation. It allows us to specify a list of roles that can access a particular method. Hence, a user will only be able to access a method if he has at least one of the given roles. This is a common use case that has already been provided by default in the Spring Security Module.

We had a bit different use case to implement, that involved authorization on a method level but with one more condition. The complete explanation of the use case is as follows.

Problem Statement

There are multiple projects and users. Every user has different permissions on different projects. Consider a service that performs CRUD operation on Project. Take for example, User1 has read permission on Project1 and read/write permission on Project2 while User2 has read/write permission on both the projects. So, if there is a method that involves any update operation on Project1, it should not be accessed by User1.

In simple terms, User1 is authorized to access the same method, when he is trying to use it for Project2, but unauthorized when he accesses it for Project1.

Solution

We used Spring AOP and Spring Expression Language (SpEL) to implement the use case. Spring AOP lets us execute some logic after/before the actual method is executed and much more … I won’t go into the details of Spring AOP. It's enough to know for now that it can intercept methods in the application using different annotations. This is the same way @Transactional annotation in spring works.

To solve the problem, we also created a custom annotation that would take roles just like @Secured takes roles. Before accessing the actual method, we intercepted it via Spring AOP and verified if the user is authorized to access that project (considering projectId or any unique project identifier is coming as an argument for that method). Using AOP we can get all the arguments coming at the execution time like their names, values, etc.

SpEL comes into play to parse the arguments and their names to get the dynamic projectId. I think it's enough theory. Let’s do some actual code.

Implementation

Let’s create a custom annotation @ProjectSecured. It takes an array of roles and name of the argument which holds the projectIdField.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProjectSecured {

Role[] roles() default {};

String projectIdField() default "";

}

The methods of ProjectService can now be annotated like below. The important thing to check here is projectIdField argument of the annotation. It depends on the actual argument and its name that is passed in the method. The projectIdField is a parsable string that will be parsed by Spring Expression Language to get the dynamic value of projectId.

@ProjectSecured(roles = {Role.READ}, projectIdField = "#project.projectId")
public Project readProject(Project project) {
// some read operation
}

@ProjectSecured(roles = {Role.READ}, projectIdField = "#id")
public Project readProject(String id) {
// some read operation
}
// pay attention to projectIdField
@ProjectSecured(roles = {Role.WRITE}, projectIdField = "#project.projectId")
public Project editProject(Project project) {
// some edit operation
}
// pay attention to projectIdField
@ProjectSecured(roles = {Role.WRITE}, projectIdField = "#id")
public Project editProject(String id) {
// some edit operation
}

The next important component to look at is the aspect that intercepts the method and validates if the user is authorized to perform the operation or not. As we can see below, the aspect intercepts all the methods annotated with ProjectSecured, fetches the method signature, arguments and passes them to SpEL Parser for getting the value at run time.

@Aspect
@Component
public class SecureProjectAspect {

@Autowired
UserService userService;

@Around("methodsAnnotatedWithProjectSecuredAnnotation()")
public Object processMethodsAnnotatedWithProjectSecuredAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ProjectSecured projectSecuredAnnotation = method.getAnnotation(ProjectSecured.class);
// parse and fetch the projectId
String projectId = (String) CustomSpringExpressionLanguageParser.
getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), projectSecuredAnnotation.projectIdField());
Role[] roles = projectSecuredAnnotation.roles();
// If user is authorized, then proceed else throw an exception
boolean isUserAuthorized = userService.isUserAuthorized(projectId, Arrays.asList(roles));
if (isUserAuthorized) {
return joinPoint.proceed();
} else {
User currentUser = userService.getCurrentUser();
throw new Exception(currentUser.getUserName() + " is not allowed to perform this operation on project with id:" + projectId);
}
}
@Pointcut("@annotation(com.example.demo.conf.ProjectSecured)")
private void methodsAnnotatedWithProjectSecuredAnnotation() {

}
}

Last, but not the least is the SpEL parser which parses the projectIdField expression and returns the dynamic projectId.

public class CustomSpringExpressionLanguageParser {
public static Object getDynamicValue(String[] parameterNames,
Object[] args, String key) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new
StandardEvaluationContext();

for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return parser.parseExpression(key).getValue(context,
Object.class);
}
}

All these components when put together will give the desired result.

@SpringBootTest
public class ProjectServiceAuthorizationTest {

@Autowired
private ProjectService projectService;

@Autowired
private UserService userService;

@Test
public void testReadProject() {

// given: current user is User1
userService.setCurrentUser("1");

// when: User1 tries to read Project1
Project project = projectService.readProject("1");
// then: operation is successful
Assertions.assertEquals("Project1", project.getName());

// when: User1 tries to read Project2
project = projectService.readProject("2");
// then: operation is successful
Assertions.assertEquals("Project2", project.getName());

// given: current user is User2
userService.setCurrentUser("2");

// when: User2 tries to read Project1
project = projectService.readProject("1");
// then: operation is successful
Assertions.assertEquals("Project1", project.getName());

// when: User2 tries to read Project2
project = projectService.readProject("2");
// then: operation is successful
Assertions.assertEquals("Project2", project.getName());
}


@Test
public void testEditProject() {

// given: current user is User1
userService.setCurrentUser("1");

// when: User1 tries to edit Project1
Executable exe = () -> projectService.editProject("1");
// then: exception is thrown
Throwable e = Assertions.assertThrows(Exception.class, exe);
e = ((UndeclaredThrowableException) e)
.getUndeclaredThrowable();
Assertions.assertEquals("User1 is not allowed to perform
this operation on project with id:1", e.getMessage());

// when: User1 tries to edit Project2
Project project = projectService.editProject("2");
// then: operation is successful
Assertions.assertEquals("New Project2", project.getName());


// given: current user is User2
userService.setCurrentUser("2");

// when: User2 tries to edit Project1
project = proherejectService.editProject("1");
// then: operation is successful
Assertions.assertEquals("New Project1", project.getName());

// when: User2 tries to edit Project2
project = projectService.editProject("2");
// then: operation is successful
Assertions.assertEquals("New Project2", project.getName());
}


@Configuration
@ComponentScan(basePackages = "com.example.demo")
static class SpringConfig {
}
}

The full code of this use case can be found here. The code here might also be relevant for some people.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store