Search code examples
springspring-mvcspring-securitymaintenance-mode

Add Maintenance Mode to Spring (Security) app


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.