Добавить режим обслуживания в приложение Spring (Security)

Я ищу способ реализовать Режим обслуживания в моем приложении Spring.

Пока приложение находится в режиме обслуживания только пользователи role = MAINTENANCE должно быть разрешено входить в систему. Все остальные перенаправляются на страницу входа.

Прямо сейчас я только что построил фильтр:

@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); 
        }
    }
}

И добавил это используя:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // omitted other stuff
        .addFilterAfter(maintenanceFilter, SwitchUserFilter.class);
}

Потому что, насколько я понял, SwitchUserFilter должен быть последним фильтром в цепочке фильтров Spring Security.

Теперь каждый запрос отменяется с ответом 503. Хотя нет доступа к странице входа.

Если я добавлю перенаправление в Фильтр, это приведет к бесконечному циклу, потому что доступ к странице входа также запрещен.

Кроме того, я не могу найти хороший способ получить роли текущих пользователей. Или я должен просто пойти с SecurityContextHolder?


Я ищу способ перенаправить каждого пользователя на страницу входа (возможно, с параметром запроса ?maintenance=true) и каждый пользователь с role = MAINTENANCE можете использовать приложение.

Таким образом, фильтр / перехватчик должен вести себя так:

if(maintenance.isEnabled()) {
    if(currentUser.hasRole(MAINTENANCE)) {
        // this filter does nothing
    } else {
        redirectTo(loginPage?maintenance=true);
    }
}

3 ответа

Решение

Теперь я нашел два похожих решения, которые работают, но место, где я внедряю код, выглядит не очень хорошо.

Для обоих я добавляю кастом RequestMatcher, которые получают @Autowired и проверяет, включен ли режим обслуживания или нет.

Решение 1:

@Component
public class MaintenanceRequestMatcher implements RequestMatcher {
    @Autowired SettingStore settingStore;

    @Override
    public boolean matches(HttpServletRequest request) {
        return settingStore.get(MaintenanceMode.KEY).isEnabled()
    }
}

И в моей конфигурации безопасности:

@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()
    // ...
}

Решение 2:

Очень похоже, но использует 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()
    // ...
}

Это выполнит denyAll() если включен режим обслуживания и текущий пользователь не имеет MY_ROLE,


Единственным недостатком является то, что я не могу установить пользовательский ответ. Я бы предпочел вернуть 503 Service Unavailable, Может быть, кто-то может понять, как это сделать.

Это своего рода дилемма курица или яйцо, вы хотите показать неавторизованным пользователям сообщение "мы находимся в режиме обслуживания...", в то время как авторизованные пользователи могут войти в систему, но вы не знаете, авторизованы ли они, пока они не войдут в систему. В идеале было бы неплохо иметь это в каком-то фильтре, но я обнаружил, что на практике мне было проще решить аналогичную проблему, применив логику после входа в систему, как в UserDetailsService,

Вот как я решил это на проекте. Когда я нахожусь в режиме обслуживания, я устанавливаю флаг для представления, чтобы показать сообщение "мы находимся в режиме обслуживания...", в глобальном заголовке или на странице входа. Таким образом, пользователи, независимо от того, кто они, знают, что это режим обслуживания. Логин должен работать как обычно.

После того, как пользователь прошел проверку подлинности, и в моем обычае UserDetailsService где их пользовательские данные загружены с их ролями, я делаю следующее:

// if we're in maintenance mode and does not have correct role
if(maintenance.isEnabled() && !loadedUser.hasRole(MAINTENANCE)) {
  throw new UnauthorizedException(..)
}
// else continue as normal

Это не красиво, но это было просто понять (что я думаю, хорошо для настройки безопасности), и это работает.

Обновить:

С вашим решением мне пришлось бы уничтожить сеанс каждого пользователя, иначе пользователь, вошедший в систему до включения режима обслуживания, все еще может работать с системой.

В нашем проекте мы не разрешаем пользователям входить в систему в режиме обслуживания. Администратор запускает задачу, которая включает сообщение "обслуживание...", с обратным отсчетом, а затем в конце мы заканчиваем сеанс каждого, используя SessionRegistry,

У меня была похожая ситуация, и я нашел этот ответ полезным. Я последовал второму подходу, и мне также удалось вернуть собственный ответ.

Вот что я сделал, чтобы вернуть индивидуальный ответ.

1- Определите метод контроллера, который возвращает необходимый настраиваемый ответ.

      @RestController
public class CustomAccessDeniedController {
    
    @GetMapping("/access-denied")
    public String getAccessDeniedResponse() {
        return "my-access-denied-page";
    }
}

2- В контексте безопасности вы должны разрешить доступ к этому URL-адресу.

      http.authorizeRequests().antMatchers("/access-denied").permitAll()

3- Создание настраиваемого обработчика исключений для отказа в доступе

      @Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private SettingStore settingStore;
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
            response.sendRedirect(request.getContextPath() + "/access-denied");
        }
    }
}

4- Зарегистрируйте обработчик исключений настраиваемого запрета доступа в конфигурации безопасности

          @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;


    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
Другие вопросы по тегам