By default, Spring Security's SecurityContext
is a thread local. So you can access it anywhere from the request thread, using the SecurityContextHolder
. From the SecurityContext
you can obtain the UserDetails
, which will have the user name.
So because the Jersey request processing occurs in the same request thread, you should be able to access this information. From a Jersey component you can use whatever service you want to obtain you actual user domain objects and just check for the same user. For example you can have something like
@Path("/accounts")
public class AccountsResource {
@Inject
private UserService userService;
@GET
@Path("/{id}")
@Produces("application/json")
public User get(@PathParam("id") Long id) {
User user = userService.getUser(id);
// if user == null throw 404
UserDetails userDetails
= (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (!userDetails.getUsername().equals(user.getUsername())) {
throw new ForbiddenException();
}
return user;
}
}
If you have a lot of endpoints like this, and you want to keep it DRY, you can use a Jersey filter to handle the access control process.
@Provider
@AccessFiltered
@Priority(Priorities.AUTHORIZATION)
public class UserAccessFilter implements ContainerRequestFilter {
@Inject
private UserService userService;
@Override
public void filter(ContainerRequestContext requestContext) {
List<String> params = requestContext.getUriInfo().getPathParameters().get("id");
Long id = Long.parseLong(params.get(0));
User user = userService.getUser(id);
// if user == null throw 404
UserDetails userDetails
= (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (!userDetails.getUsername().equals(user.getUsername())) {
throw new ForbiddenException();
}
}
}
Now with the Name Binding Annotation @AccessFiltered
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AccessFiltered {
}
We can just filter the methods we annotated
@GET
@Path("/{id}")
@AccessFiltered
@Produces("application/json")
public User get(@PathParam("id") Long id) {
}
Now suppose we want access to that User
, and we don't want to have to hit the DB again to retrieve it in our resource method. It's possible to put the User
we obtained inside the filter, into the request context (a full example here)
public class UserAccessFilter implements ContainerRequestFilter {
public static final String USER_FILTER_PROPERTY = "UserAccessFilter.User";
@Override
public void filter(ContainerRequestContext requestContext) {
...
User user = userService.getUser(id);
...
requestContext.setProperty(USER_FILTER_PROPERTY, user);
}
}
Then we can just inject the User
into our resource method
@GET
@Path("/{id}")
@AccessFiltered
@Produces("application/json")
public User get(@Context User user) {
return user;
}
There's one more step to configure this (i.e. the Factory
- see the previous link). You could also create a custom annotation for injecting the user, but that gets a bit more complicated.
I'm sure this type of access control could be implemented using Spring Security's ACL feature, but for me being a Jersey user, this way is more intuitive for me.