This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

×

This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

×

Spring Security using Beans in Spring Expression Language

Spring Security is a great way of implementing security in your Spring based system. The standard @Secured annotation has been used for years in many projects for simple role based authorisation:

@Secured({"ROLE_ADMIN"})
public String showProducts() {
    return "secure/products";
}

This is all fine and well for simple security measures where you have fairly broad rules about what kind of user can access fairly general resources.

What happens if things get a bit more complicated? Let's imagine you have one type of user who can login to a system and they have a role of ROLE_ADMIN. All of these users can view all products. However, only users who own a particular product can edit it. How can the very broad Spring Security role based annotations help us?

@Secured({"ROLE_ADMIN"})
public String editProduct(Long productId) {
    ...do something with productId...
    return "secure/editproduct";
}

I want the specific user who is accessing this method to only be able to edit products they own. I could do something like this:

@Secured({"ROLE_ADMIN"})
public String editProduct(Long productId, Principal auth) {
    final UserDetails userDetails = (UserDetails)((UsernamePasswordAuthenticationToken)auth).getPrincipal();
    Product product = productDao.getProduct(productId);
    if (!product.isEditableBy(userDetails)) {
          throw new AccessDeniedException("Cannot access this product");
    }
    ...do something with productId...
    return "secure/editproduct";
}

The problem here is that now my nice small edit product method needs to be aware of the authentication framework that is going on around it. Which means my unit tests get more complicated as I need to setup a Principal object every time I want to test anything to do with restricted access to products. I'd like to keep the nice clean split between the security and the edit product code that I had in my first draft, but with more fine grained access controls still.

@PreAuthorize("@securityService.canEdit(authentication, #productId)")
public String editProduct(Long productId) {
    ...do something with productId...
    return "secure/editproduct";
}

The solution

By swapping out the @Secured annotation for the more modern and powerful @PreAuthorize annotation, we can use Spring Expression Language. If you were to replace the @Secured("{ROLE_ADMIN}") with @PreAuthorize("hasRole('ROLE_ADMIN')"), it would be equivalent.

  • Using the "@" symbol in Spring EL you can reference a Spring Bean.
  • Using the "#" symbol in Spring EL you can reference a parameter on the method you are securing.

Combining these two lets you pass parameters to a method along with some of the parameters that are available by default in the @PreAuthorize annotation such as:

  • principal - Allows direct access to the principal object representing the current user
  • authentication - Allows direct access to the current Authentication object obtained from the SecurityContext

We have now given the securityService bean all of the information it needs to make an authorisation decision for us.

@Service("securityService")
public class SecurityServiceImpl implements SecurityService {
   
    @Autowired
    private ProductDao productDao;
    
    @Override
    public boolean canEdit(final Principal auth, final Long productId) {
        final UserDetails userDetails = (UserDetails)((UsernamePasswordAuthenticationToken)auth).getPrincipal();
        Product product = productDao.getProduct(productId);
        return product.isEditableBy(userDetails);
    }
}

We can now unit test this SecurityServiceImpl code in isolation from the other business logic and apply it in as many places as we like.

Conclusion

By splitting out your security into a service that is wired in with annotations, you can simplify your business logic and hence make easier to understand unit tests. You must still be careful that your security is tested at some point of course but that is probably better done in an integration test or acceptance test.