Lesson 4: A Custom AccessDecisionVoter
1. Goals
The goal of this lesson is to go beyond the standard
voters and show how we can set up a custom one to implement an
interesting scenario.
2. Lesson Notes
The relevant module you need to import when you're starting with this lesson is: m9-lesson4
If you want to skip and see the complete implementation, feel free to jump ahead and import: m9-lesson5
The credentials used in the code of this lesson are:
admin/pass (in-memory) (has the ADMIN role)
2.1. A Stricter Access Decision Manager
Let's start by changing the AccessDecisionManager implementation from AffirmativeBased to the more stricter UnanimousBased:
@Bean public AccessDecisionManager unnanimous(){ List<AccessDecisionVoter<? extends Object>> voters = Lists.newArrayList( new RoleVoter(), new AuthenticatedVoter(), new WebExpressionVoter()); return new UnanimousBased(voters); }
Now, let's wire it in:
.anyRequest().authenticated().accessDecisionManager(unnanimous())
And let's secure /secured with an extra role:
.antMatchers("/secured").access("hasRole('ADMIN')")
We can now try to access /secured and debug through the decision flow to see the stricter voting run.
2.2. The Custom Scenario
We're going to explore the following scenario - we need to be able to lock users out and have that lockout apply in real-time (not after a login).
The need for this kind of real-time lockout is simple - if a user
is locked out, then they’re a serious security concern so we need to
make sure that their current session cannot be used to do any damage.
Before we start, not that there are multiple ways to implement this kind of scenario - this is just one of them.
Let’s first create the new voter:
public class RealTimeLockVoter implements AccessDecisionVoter<Object> { ...
Now, before implementing the actual voting logic, let’s create the very simplistic cache for users that are locked out:
public final class LockedUsers { private static final Set<String> lockedUsersSets = Sets.newHashSet(); private LockedUsers() {} public static final boolean isLocked(final String username) { return lockedUsersSets.contains(username); } public static final void lock(final String username) { lockedUsersSets.add(username); } }
Finally - let’s get back to the voting logic:
if (LockedUsers.isLocked(authentication.getName())) { return ACCESS_DENIED; } return ACCESS_GRANTED;
And we’re done - simple and to the point.
Now if we go through a simple scenario
- login with admin (so that we can access /secured)
- then (with the help of the Display view in Eclipse) we add the user to the locked cache
- and refresh => we're locked out
2.3. Upgrade Notes
As we've already mentioned in the previous lessons, in Spring Boot 2 it is required to encode the passwords. It means that they can not be stored anymore in a plain text format. Therefore, the user accounts should be created with the encoded passwords:
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth. inMemoryAuthentication().passwordEncoder(passwordEncoder()) .withUser("user").password(passwordEncoder().encode("pass")).authorities("USER").and() .withUser("admin").password(passwordEncoder().encode("pass")).authorities("ADMIN"); }
Naturally, the corresponding bean should be added into the context:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }