Spring Security (previously Acegi) gives you an awesome AOP way of locking down methods in your Java application. And well-placed lock down is a special power indeed. However, Spring disappoints a bit on this score because it turns out its default behavior is to ‘or’ the roles listed in @Secured annotation. You’d think the default would be the stricter ‘and’, but alas. But never fear, there is hope.
Updated (21 June 2012):
The easy way
This article can be simplified into this line of code:
There are other combo options as documented by Spring.
If you wish to go on, enjoy the custom code and xml config that follows: :)
There is always hope.
Note: My experimentation with this and the line numbers in files mentioned here are from spring-security-core/3.0.7.RELEASE.
Your authorities populator or however you’re getting roles assigned to your user will eventually place them in the place that you can get to them programmatically:
And here is where the @Secured annotation will eventually look for them (AbstractSecurityInterceptor.java:204):
This collection of
GrantedAuthorities is are the roles that have been assigned to the user associated with your request to a method.
When you want only users with certain authorities to access methods, you apply the
@Secured annotation to the method needing protection. Also note that you can specify multiple roles in this annotation:
1 2 3 4
Again, to note what is important and initially stunned me: By default, you just need one of these roles (either role1 OR role2) to make it into the innards of this awesomeness. This is because of the default configuration. From the Spring Security docs:
The default strategy is to use an AffirmativeBased AccessDecisionManager with a RoleVoter and an AuthenticatedVoter.
Role Name Prefix
Another point worth making is about the prefix required for roles. Roles that are counted by the
RoleVoter are only seen as roles if they start with the specified prefix. By default,
RoleVoter sees this prefix as “ROLE_”. It is settable, but be warned that your roles will count for nothing and your
@Secured method will be totally exposed unless it looks something like this:
1 2 3 4
Votes if any ConfigAttribute#getAttribute() starts with a prefix indicating that it is a role. The default prefix string is
ROLE_, but this may be overridden to any value. It may also be set to empty, which means that essentially any attribute will be voted on. As described further below, the effect of an empty prefix may not be quite desirable.
The 2 Keys
There are two key interfaces that Spring Security is using to determine how you want your roles checked that you specify in the
- AccessDecisionManager implemented as AffirmativeBased, ConsensusBased or UnanimousBased
- AccessDecisionVoter, implemented as RoleVoter
The decision manager is the class that has a collection of voters. The decision manager orchestrates the voters and asks each in turn whether the requesting user should be let through the
@Secured annotation or denied. By default, the
AffirmativeBased manager is used.
Each decision manager functions differently in how it counts the votes of its voters:
- AffirmativeBased - if any voter comes votes ‘yes’ or ‘access granted’, the manager allows access
- ConsensusBased - this manager is majority rules. There can be votes ‘yes’ and ‘no’, and as long as ‘yes’ votes outnumber ‘no’ votes, access is allowed
- UnanimousBased - this manager requires every voter to vote ‘yes’ or else access is denied
Since this discussion is based around roles, we only care to use Spring’s
RoleVoter. Here’s part of its implementation, starting on line 98:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
vote() method goes through each role specified in the
@Secured annotation and for each of those checks whether the user has that role. If he does, the voter returns its vote as ‘yes’. Got that? At the first match, the vote is yes. This is a logical ‘or’.
Making the role vote a logical ‘and’
I don’t want my roles ‘or’ed together. I want the uesr to be required to have them all in combination or I will deny him access. Since we’ve already shown that the default
RoleVoter uses a logical ‘or’ operation, I guess we’ll need to write our own role voter that uses a logical ‘and’. Potentially, that might be called ‘HasAllRolesVoter’, and might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
It looks much like
RoleVoter. It extends
RoleVoter and overrides
vote(), which now goes through the roles on the
@Secured annotation, checks each against the roles of the user, incrementing a count with each match. If, in the end, there are as many matches as roles specified, we’ve found them all. This is just a potential implementation. There are probably many better was to get this done.
So now can we do a logical ‘and’? Yes. There are 3 ways of accomplishing this (and probably more):
- AffirmativeBased w/ HasAllRolesVoter
- ConsensusBased w/ HasAllRolesVoter
- UnanimousBased w/ RolesVoter
Note that in the last option
UnanimousBased access manager can use just the plain jane
RolesVoter. This is because
UnanimousBased calls the
RoleVoter up for vote for each individual role (one by one via the
singleAttributeList) as opposed to the roles as a collection. Thus, that combination ignores the fact that
RoleVoter normally ‘or’s matching roles together.
So, there really hasn’t been too much to this solution. But as with most frameworks of this flexibility and complexity, the key is knowing how to actually specify that you want it to work this way. To specify a
UnanimousBased access manager using the plain
RoleVoter (my eventual solution for my own problem) looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Smooth as butter. As with most Spring solutions, the answer was in an xml configuration. Oh boy.