96

I would like to create a class that adds custom methods for use in spring security expression language for method-based authorization via annotations.

For example, I would like to create a custom method like 'customMethodReturningBoolean' to be used somehow like this:

  @PreAuthorize("customMethodReturningBoolean()")
  public void myMethodToSecure() { 
    // whatever
  }

My question is this. If it is possible, what class should I subclass to create my custom methods, how would I go about configuring it in the spring xml configuration files and come someone give me an example of a custom method used in this way?

Paul D. Eden
  • 18,002
  • 18
  • 54
  • 62
  • 2
    I don't have time to type an answer right now but I followed this guide and it worked brilliantly: https://www.baeldung.com/spring-security-create-new-custom-security-expression I'm using Spring Security 5.1.1. – Paul Nov 27 '18 at 20:22

3 Answers3

185

None of the mentioned techniques will work anymore. It seems as though Spring has gone through great lengths to prevent users from overriding the SecurityExpressionRoot.

EDIT 11/19/14 Setup Spring to use security annotations:

<beans ... xmlns:sec="http://www.springframework.org/schema/security" ... >
...
<sec:global-method-security pre-post-annotations="enabled" />

Create a bean like this:

@Component("mySecurityService")
public class MySecurityService {
    public boolean hasPermission(String key) {
        return true;
    }
}

Then do something like this in your jsp:

<sec:authorize access="@mySecurityService.hasPermission('special')">
    <input type="button" value="Special Button" />
</sec:authorize>

Or annotate a method:

@PreAuthorize("@mySecurityService.hasPermission('special')")
public void doSpecialStuff() { ... }

Additionally, you may use Spring Expression Language in your @PreAuthorize annotations to access the current authentication as well as method arguments.

For example:

@Component("mySecurityService")
public class MySecurityService {
    public boolean hasPermission(Authentication authentication, String foo) { ... }
}

Then update your @PreAuthorize to match the new method signature:

@PreAuthorize("@mySecurityService.hasPermission(authentication, #foo)")
public void doSpecialStuff(String foo) { ... }
James Watkins
  • 4,252
  • 5
  • 26
  • 37
  • Any way to give MySecurityService a handle on an existing `Authentication` object? – Bosh May 29 '13 at 17:52
  • 6
    @Bosh in your hasPermission method, you can use `Authentication auth = SecurityContextHolder.getContext().getAuthentication();` to get the current authentication token. – James Watkins May 30 '13 at 19:13
  • 2
    Thanks James for your answer. Do I have to define mySecurityService in spring config file? – WowBow Jun 17 '13 at 21:45
  • And is @PreAuthorize a custom annotation? – WowBow Jun 17 '13 at 21:50
  • 2
    You don't need to define mySecurityService in any XML file if you have a component-scan setup for the package that the service is in. If you don't have a matching component-scan, then you must use an xml bean definition. @PreAuthorize comes from org.springframework.security – James Watkins Jun 18 '13 at 15:08
  • James, simply great bro !! – shaILU Jun 21 '13 at 14:46
  • James, simply great bro !! But id doesn't worked for me. I created a bean as component and trying to use in PreAuthorize via my component class. – shaILU Jun 21 '13 at 15:25
  • 3
    You may need to specify the name of the bean to the annotation like this: @Component("mySecurityService") or use the @Named annotation. – James Watkins Jul 01 '13 at 01:50
  • I added the @Component("mySecurityService") and it worked! thanks! – Jonathan Oct 16 '13 at 08:37
  • @James Watkins : I have used `@PreAuthorize` example shown above but hasPermission method of MySecurityService is not getting called. In xml I have defined global security pre-post annotation enabled as well. Code is something like : @RequestMapping...then @PreAuthorize...then method. Method is working fine but @PreAuthorize is having no impact. – VJS Nov 18 '14 at 08:44
  • 1
    @VJS Please see the edit I made. You will need to configure spring to use these annotations. I'm surprised nobody else complained about this important missing detail :) – James Watkins Nov 19 '14 at 16:31
  • @James Watkins : Yes, I googled it and found solution. But thanks for your effort. This solution is very clean and easy to understand. It saved my lot of time. – VJS Nov 20 '14 at 14:09
  • @James Watkins : I am getting : org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'mySecurityService' is defined – The Java Guy Mar 12 '15 at 03:22
  • Incredibly simple and straightforward solution! You saved me a lot of time and for that I thank you! – XMight Jul 15 '16 at 11:58
  • Spent a while trying to get the custom expression root gunk to work. Your solution is far simpler to get to work thanks. – JCS Sep 14 '16 at 10:09
  • Is there any way I can make 'authentication' argument implicit? – dsk Oct 11 '16 at 06:21
  • @DhananjayK You may remove the argument and fetch the Authentication object statically. Caution: I'm not sure how this works if you are forking threads: Authentication auth = SecurityContextHolder.getContext().getAuthentication(); – James Watkins Oct 11 '16 at 15:45
  • @James Watkins Can I use mySecurityService on JSPs to hide or show links? – GD_Java Aug 21 '17 at 12:57
  • @GD_Java Yes, using the spring security taglib https://docs.spring.io/spring-security/site/docs/3.1.x/reference/taglibs.html – James Watkins Aug 21 '17 at 15:18
  • @James Watkins I wrote a component as you suggested and calling it from JSP like this . But it is not worming any more. The only difference is my hasPermission accept one more object which is session. – GD_Java Aug 25 '17 at 15:33
35

You'll need to subclass two classes.

First, set a new method expression handler

<global-method-security>
  <expression-handler ref="myMethodSecurityExpressionHandler"/>
</global-method-security>

myMethodSecurityExpressionHandler will be a subclass of DefaultMethodSecurityExpressionHandler which overrides createEvaluationContext(), setting a subclass of MethodSecurityExpressionRoot on the MethodSecurityEvaluationContext.

For example:

@Override
public EvaluationContext createEvaluationContext(Authentication auth, MethodInvocation mi) {
    MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(auth, mi, parameterNameDiscoverer);
    MethodSecurityExpressionRoot root = new MyMethodSecurityExpressionRoot(auth);
    root.setTrustResolver(trustResolver);
    root.setPermissionEvaluator(permissionEvaluator);
    root.setRoleHierarchy(roleHierarchy);
    ctx.setRootObject(root);

    return ctx;
}
sourcedelica
  • 23,226
  • 6
  • 60
  • 71
  • Hmm, sounds like a good idea, but all of the properties of *DefaultMethodSecurityExpressionHandler* are private without accessors, so I was curious how you extended the class without any ugly reflection. Thanks. – Joseph Lust Aug 23 '11 at 15:50
  • 1
    You mean trustResolver, etc? Those all have setters in DefaultMethodSecurityExpressionHandler (at least in Spring Security 3.0) See: http://static.springsource.org/spring-security/site/apidocs/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.html – sourcedelica Aug 24 '11 at 19:41
  • 3
    @ericacm How do you get around `MethodSecurityExpressionRoot` being *package-private*? – C. Ross Apr 30 '12 at 17:03
14

Thanks ericacm, but it does not work for a few reasons:

  • The properties of DefaultMethodSecurityExpressionHandler are private (reflection visibility kludges undesirable)
  • At least in my Eclipse, I can't resolve a MethodSecurityEvaluationContext object

The differences are that we call the existing createEvaluationContext method and then add our custom root object. Finally I just returned an StandardEvaluationContext object type since MethodSecurityEvaluationContext would not resolve in the compiler (they are both from the same interface). This is the code that I now have in production.

Make MethodSecurityExpressionHandler use our custom root:

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler  {

    // parent constructor
    public CustomMethodSecurityExpressionHandler() {
        super();
    }

    /**
     * Custom override to use {@link CustomSecurityExpressionRoot}
     * 
     * Uses a {@link MethodSecurityEvaluationContext} as the <tt>EvaluationContext</tt> implementation and
     * configures it with a {@link MethodSecurityExpressionRoot} instance as the expression root object.
     */
    @Override
    public EvaluationContext createEvaluationContext(Authentication auth, MethodInvocation mi) {
        // due to private methods, call original method, then override it's root with ours
        StandardEvaluationContext ctx = (StandardEvaluationContext) super.createEvaluationContext(auth, mi);
        ctx.setRootObject( new CustomSecurityExpressionRoot(auth) );
        return ctx;
    }
}

This replaces the default root by extending SecurityExpressionRoot. Here I've renamed hasRole to hasEntitlement:

public class CustomSecurityExpressionRoot extends SecurityExpressionRoot  {

    // parent constructor
    public CustomSecurityExpressionRoot(Authentication a) {
        super(a);
    }

    /**
     * Pass through to hasRole preserving Entitlement method naming convention
     * @param expression
     * @return boolean
     */
    public boolean hasEntitlement(String expression) {
        return hasRole(expression);
    }

}

Finally update securityContext.xml (and make sure it's referenced from your applcationContext.xml):

<!-- setup method level security using annotations -->
<security:global-method-security
        jsr250-annotations="disabled"
        secured-annotations="disabled"
        pre-post-annotations="enabled">
    <security:expression-handler ref="expressionHandler"/>
</security:global-method-security>

<!--<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">-->
<bean id="expressionHandler" class="com.yourSite.security.CustomMethodSecurityExpressionHandler" />

Note: the @Secured annotation will not accept this override as it runs through a different validation handler. So, in the above xml I disabled them to prevent later confusion.

Community
  • 1
  • 1
Joseph Lust
  • 17,180
  • 7
  • 70
  • 72