Annotation-based Role access security for Spring Boot - roles configuration on MongoDB
Working on a research project in my company, I needed a nice way to define Security Access to my services. Java Annotation is the answer. Read the story to see the code details
The system is based on a microservices architecture using spring boot as actuator. Users are registered on MongoDB in User collection with the following schema example:
> db.User.find().pretty()
{
"_id" : ObjectId("56351188da060d8a43bfe108"),
"username" : "dino.lupo",
"hashedPassword" : "$2a$10$mRDKUyd01khAgBoOFmFei.tcY59YjzO8FXSjz4fonnLUlt7TAl8R.",
"email" : "dino.lupo@gmail.com",
"firstname" : "Dino",
"lastname" : "Lupo",
"roles" : [
"write-user",
"read-user",
"admin"
]
}
I needed a way to annotate my spring Controller methods with a @Role
annotation as follows:
@Controller
@EnableAutoConfiguration
public class UserController {
// ...
/**
* Return the list of all Users that are registred
* @return
*/
@Role(access="read-user")
@RequestMapping(value = "/protected/allUsers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<Object> getAllUsers(HttpServletRequest request, HttpServletResponse response) {
// ...
}
// ...
}
In the previous example, I wanted to permit the access to the web method only to connected users with a “read-user” Role configured in my MongoDB.
The code is at the following GitHub repo GitHub Repository
It is composed by two annotations definition and some other configuration classes:
@EnableRoleChecking
@Inherited
@Import(RoleConfiguration.class)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface EnableRoleChecking {
}
This class is needed by the root Spring Boot Application to configure the Interception Handlers.
@Role
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Role {
String access() default "guest";
}
The @Role
Annotation is needed if you want to restrict access to a method only by connected user that has the right role in the database.
The RoleConfiguration
class declares the SecurityInterceptor Bean that is needed to check the roles into the DB and the RoleConfigurationProperties
declares the field needed by the SecurityHandler to work properly.
- You have to Add the following required properties
in Spring Boot’s
application.properties
orapplication.yml
Example:
spring:
data:
mongodb:
database: UserRegistrationDB
host: localhost
port: 27017
temotec:
annotations:
security:
collection: User
role-path: roles
username-path: username
“collection” is the name of the MongoDB collection where the Users are stored. “role-path” is the name of the property where roles are stored for the User. “username-path” is the name of the property that corresponds to the username of the connected user.
The most important class is the SecurityInterceptor that extends the HandlerInterceptorAdapter, there are a lot of resource on the web on how this works, but essentially it declares 3 methods PreHandle, PostHandle and AfterCompletion that you can you to execute code before, after and at the end of the intercepted method. In the SecurityInterceptor I override only the PreHandle to verify the role:
@Component
public class SecurityInterceptor extends HandlerInterceptorAdapter {
@Autowired
MongoClient mongoClient;
@Autowired
RoleConfigurationProperties roleProperties;
@Autowired
MongoProperties mongoProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String connectedUser = request.getUserPrincipal() == null ? null : request.getUserPrincipal().getName();
// if no user is logged in, then exit
if (connectedUser == null)
return false;
// cast the annotated spring handler method
HandlerMethod handlerMethod = (HandlerMethod) handler;
// get method using reflection
Method method = handlerMethod.getMethod();
// check if the method is annotated
if (handlerMethod.getMethod().isAnnotationPresent(Role.class)) {
Annotation annotation = method.getAnnotation(Role.class);
Role casRole = (Role) annotation;
System.out.printf("Access :%s\n", casRole.access());
MongoDatabase db = mongoClient.getDatabase(mongoProperties.getDatabase());
MongoCollection<Document> coll = db.getCollection(roleProperties.getCollection(), Document.class);
Bson filter = and( eq(roleProperties.getUsernamePath(), connectedUser),
eq(roleProperties.getRolePath(), casRole.access())
);
long found = coll.count(filter);
System.out.printf("Found: %d\n", found);
if (0 == found) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
return true;
}
}
The interceptor is quite straightforward, it uses the mongodb driver to find if the connected user has the annotated role into his own record.
To see how to I used it, go to the GitHub repository and read the instructions Readme