Issue
I'm looking for a way to implement a Maintenance Mode in my Spring app.
While the app is in Maintenance Mode only users role = MAINTENANCE
should be allowed to log in. Everyone else gets redirected to login page.
Right now I just built a Filter:
@Component
public class MaintenanceFilter extends GenericFilterBean {
@Autowired SettingStore settings;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
} else {
chain.doFilter(request, response);
}
}
}
And added it using:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// omitted other stuff
.addFilterAfter(maintenanceFilter, SwitchUserFilter.class);
}
Because as far as I figured out SwitchUserFilter
should be the last filter in Spring Security's filter chain.
Now every request gets canceled with a 503 response. Though there's no way to access the login page.
If I add a redirect to the Filter, this will result in an infinite loop, because access to login page is also denied.
Additionally I can't figure out a nice way to get the current users roles. Or should I just go with SecurityContextHolder
?
I'm looking for a way to redirect every user to the login page (maybe with a query param ?maintenance=true
) and every user with role = MAINTENANCE
can use the application.
So the Filter / Interceptor should behave like:
if(maintenance.isEnabled()) {
if(currentUser.hasRole(MAINTENANCE)) {
// this filter does nothing
} else {
redirectTo(loginPage?maintenance=true);
}
}
Solution
I now found two similar solutions which work, but the place where I inject the code doesn't look that nice.
For both I add a custom RequestMatcher
, which get's @Autowired
and checks if Maintenance Mode is enabled or not.
Solution 1:
@Component
public class MaintenanceRequestMatcher implements RequestMatcher {
@Autowired SettingStore settingStore;
@Override
public boolean matches(HttpServletRequest request) {
return settingStore.get(MaintenanceMode.KEY).isEnabled()
}
}
And in my Security Config:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.requestMatchers(maintenanceRequestMatcher).hasAuthority("MY_ROLE")
.anyRequest().authenticated()
// ...
}
Solution 2:
Very similar, but uses HttpServletRequest.isUserInRole(...)
:
@Component
public class MaintenanceRequestMatcher implements RequestMatcher {
@Autowired SettingStore settingStore;
@Override
public boolean matches(HttpServletRequest request) {
return settingStore.get(MaintenanceMode.KEY).isEnabled() && !request.isUserInRole("MY_ROLE");
}
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.requestMatchers(maintenanceRequestMatcher).denyAll()
.anyRequest().authenticated()
// ...
}
This will perform a denyAll()
if Maintenance Mode is enabled and the current user does not have MY_ROLE
.
The only disadvantage is, that I cannot set a custom response. I'd prefer to return a 503 Service Unavailable
. Maybe someone can figure out how to do this.
Answered By - Benjamin M