Статьи

su и sudo в приложениях Spring Security

Давным-давно я работал над проектом, у которого была довольно мощная функция. Было две роли: пользователь и руководитель. Supervisor может изменить любой документ в системе любым способом, в то время как пользователи гораздо более ограничены ограничениями рабочего процесса. Когда у обычного пользователя возникли проблемы с документом, который в данный момент редактируется и хранится в сеансе HTTP, супервизор может войти, переключиться в специальный режим супервизора и обойти все ограничения. Полная свобода Тот же компьютер, та же клавиатура, тот же сеанс HTTP. Только специальный флаг, который супервизор может установить, введя секретный пароль. Как только супервизор закончил, он или она может сбросить этот флаг и снова включить обычные ограничения.

Эта функция работала хорошо, но была плохо реализована. Доступность каждого поля ввода зависела от этого флага режима супервизора . Бизнес-методы были загрязнены в десятках мест isSupervisorMode() . И помните, что если супервизор просто вошел в систему с использованием обычных учетных данных, этот режим был бы неявным, поэтому ограничения безопасности были в основном дублированы.

Еще один интересный случай использования возникает, когда наше приложение имеет широкие возможности настройки с большим количеством ролей безопасности. Рано или поздно вы столкнетесь с аномалией (ОК, ошибка ), которую вы просто не сможете воспроизвести, имея разные привилегии. Возможность войти в систему под этим конкретным пользователем и осмотреться может быть большой победой. Конечно, вы не знаете пароли своих пользователей ( не так ли? ). UNIX-подобные системы нашли решение этой проблемы: команды su ( switch user ) и sudo . Удивительно, но Spring Security поставляется со встроенным SwitchUserFilter который в принципе имитирует su в веб-приложениях. Давайте попробуем!

Все, что вам нужно, это объявить пользовательский фильтр:

1
2
3
4
5
<bean id="switchUserProcessingFilter"
       class="org.springframework.security.web.authentication.switchuser.SwitchUserFilter">
    <property name="userDetailsService" ref="userDetailsService"/>
    <property name="targetUrl" value="/"/>
</b:bean>

и указав на него в <http> конфигурации:

1
2
3
4
<http auto-config="true" use-expressions="true">
    <custom-filter position="SWITCH_USER_FILTER" ref="switchUserProcessingFilter" />
    <intercept-url pattern="/j_spring_security_switch_user" access="hasRole('ROLE_SUPERVISOR')"/>
    ...

Это оно! Обратите внимание, что я /j_spring_security_switch_user шаблон URL /j_spring_security_switch_user . Как вы уже догадались, вы входите в систему как другой пользователь, поэтому мы хотим, чтобы этот ресурс был хорошо защищен. По умолчанию j_username имя параметра j_username . После внесения вышеуказанных изменений в ваше веб-приложение и входа в систему с пользователем, имеющим ROLE_SUPERVISOR можно просто перейти к:

1
/j_spring_security_switch_user?j_username=bob

И автоматически вы входите в систему как bob — при условии, что такой пользователь существует. Здесь пароль не требуется. Когда вы закончите выдавать себя за него, просмотр /j_spring_security_exit_user восстановит ваши предыдущие учетные данные. Конечно, все эти URL-адреса настраиваются. SwitchUserFilter не SwitchUserFilter в справочной документации, но это очень полезный инструмент, если его использовать с осторожностью.

Действительно с большой силой … Предоставление возможности входа даже самым доверенным пользователям, как и любому другому произвольному пользователю, звучит довольно рискованно. Представить такую ​​функцию в Facebook невозможно! ( хорошо … ) Таким образом, отслеживание и аудит становится основным требованием.

В первую очередь я обычно добавляю небольшой фильтр сервлетов сразу после фильтра Spring Security, который добавляет имя пользователя в MDC :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.slf4j.MDC;
  
public class UserNameFilter implements Filter {
  
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        final String userName = authentication.getName();
        final String fullName = userName + (realName != null ? " (" + realName + ")" : "");
  
        MDC.put("user", fullName);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("user");
        }
    }
  
    private String findSwitchedUser(Authentication authentication) {
        for (GrantedAuthority auth : authentication.getAuthorities()) {
            if (auth instanceof SwitchUserGrantedAuthority) {
                return ((SwitchUserGrantedAuthority)auth).getSource().getName();
            }
        }
        return null;
    }
  
    //...
}

Просто не забудьте добавить его в web.xml после Spring Security. На этом этапе вы можете ссылаться на ключ "user" например, в logback.xml :

1
<pattern>%d{HH:mm:ss.SSS} | %-5level | %X{user} | %thread | %logger{1} | %m%n%rEx</pattern>

Видите фрагмент %X{user} ? Каждый раз, когда вошедший в систему пользователь делает что-то в системе, которое вызывает оператор log, вы увидите имя этого пользователя:

1
2
3
21:56:55.074 | DEBUG | alice | http-bio-8080-exec-9 | ...
//...
21:56:57.314 | DEBUG | bob (alice) | http-bio-8080-exec-3 | ...

Второе утверждение журнала интересно. Если вы посмотрите на findSwitchedUser() выше, станет очевидно, что alice , будучи супервизором, переключилась на пользователя bob и теперь просматривает его от имени.

Иногда вам нужна еще более сильная система аудита. К счастью, среда Spring имеет встроенную инфраструктуру событий, и мы можем использовать преимущества AuthenticationSwitchUserEvent отправленные как при переключении пользователя, так и при выходе из этого режима:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Service
public class SwitchUserListener
       implements ApplicationListener<AuthenticationSwitchUserEvent> {
  
    private static final Logger log = LoggerFactory.getLogger(SwitchUserListener.class);
  
    @Override
    public void onApplicationEvent(AuthenticationSwitchUserEvent event) {
        log.info("User switch from {} to {}",
                event.getAuthentication().getName(),
                event.getTargetUser().getUsername());
    }
}

Конечно, вы можете заменить простую регистрацию любой бизнес-логикой, которую вы пожелаете, например, сохранить такое событие в базе данных или отправить электронное письмо сотруднику службы безопасности.

Таким образом, мы знаем, как войти в систему как другой пользователь в течение определенного периода времени, а затем выйти из такого режима. Но что если нам понадобится « sudo », то есть только один HTTP-запрос от имени другого пользователя? Конечно, мы можем переключиться на этого пользователя, выполнить этот запрос и затем выйти. Но это кажется слишком тяжелым и громоздким. Такое требование может появиться, когда клиентская программа обращается к нашему API и хочет видеть данные как другого пользователя (подумайте о тестировании сложных ACL).

Добавление специального заголовка HTTP для обозначения такого специального имитирующего запроса звучит разумно. Он работает только на время одного запроса, предполагая, что клиент уже аутентифицируется, например, используя cookie-файл JSESSIONID. К сожалению, это не поддерживается Spring Security, но его легко реализовать поверх SwitchUserFilter :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SwitchUserOnceFilter extends SwitchUserFilter {
  
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
  
        final String switchUserHeader = request.getHeader("X-Switch-User-Once");
        if (switchUserHeader != null) {
            trySwitchingUserForThisRequest(chain, request, res, switchUserHeader);
        } else {
            super.doFilter(req, res, chain);
        }
    }
  
    private void trySwitchingUserForThisRequest(FilterChain chain, HttpServletRequest request, ServletResponse response, String switchUserHeader) throws IOException, ServletException {
        try {
            proceedWithSwitchedUser(chain, request, response, switchUserHeader);
        } catch (AuthenticationException e) {
            throw Throwables.propagate(e);
        }
    }
  
    private void proceedWithSwitchedUser(FilterChain chain, HttpServletRequest request, ServletResponse response, String switchUserHeader) throws IOException, ServletException {
        final Authentication targetUser = attemptSwitchUser(new SwitchUserRequest(request, switchUserHeader));
        SecurityContextHolder.getContext().setAuthentication(targetUser);
  
        try {
            chain.doFilter(request, response);
        } finally {
            final Authentication originalUser = attemptExitUser(request);
            SecurityContextHolder.getContext().setAuthentication(originalUser);
        }
  
    }
  
}

Единственное отличие от оригинального SwitchUserFilter состоит в том, что если присутствует "X-Switch-User-Once" , мы переключаем учетные данные на пользователя, обозначенного значением этого заголовка — однако только на время одного HTTP-запроса. SwitchUserFilter предполагает, что имя пользователя для переключения находится под параметром j_username поэтому мне пришлось немного обмануть с SwitchUserRequest обертки SwitchUserRequest :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private class SwitchUserRequest extends HttpServletRequestWrapper {
  
    private final String switchUserHeader;
  
    public SwitchUserRequest(HttpServletRequest request, String switchUserHeader) {
        super(request);
        this.switchUserHeader = switchUserHeader;
    }
  
    @Override
    public String getParameter(String name) {
        switch (name) {
            case SPRING_SECURITY_SWITCH_USERNAME_KEY:
                return switchUserHeader;
            default:
                return super.getParameter(name);
        }
    }
}

И наш собственный « sudo » на месте! Вы можете проверить это, например, используя curl :

1
2
3
$ curl localhost:8080/books/rest/book \
    -H "X-Switch-User-Once: bob" \
    -b "JSESSIONID=..."

Конечно, без cookie-файла JSESSIONID система не позволит нам войти. Сначала мы должны войти в систему и иметь специальные привилегии для доступа к функциональности sudo . Переключение пользователя — удобный и довольно мощный инструмент. Если вы хотите попробовать это на практике, посмотрите рабочий пример на GitHub .

Справка: su и sudo в приложениях Spring Security от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей .