«1. Введение

Большую часть времени при защите веб-приложения Spring или REST API инструментов, предоставляемых Spring Security, более чем достаточно, но иногда мы ищем более конкретное поведение.

В этом руководстве мы напишем собственный AccessDecisionVoter и покажем, как его можно использовать для абстрагирования логики авторизации веб-приложения и отделения ее от бизнес-логики приложения.

2. Сценарий

Чтобы продемонстрировать, как работает AccessDecisionVoter, мы реализуем сценарий с двумя типами пользователей, ПОЛЬЗОВАТЕЛЬ и АДМИНИСТР, в котором ПОЛЬЗОВАТЕЛЬ может получить доступ к системе только в четные минуты, а АДМИН. всегда иметь доступ.

3. Реализации AccessDecisionVoter

Сначала мы опишем несколько реализаций, предоставляемых Spring, которые будут участвовать вместе с нашим пользовательским избирателем в принятии окончательного решения об авторизации. Затем мы рассмотрим, как реализовать собственный избиратель.

3.1. Реализации AccessDecisionVoter по умолчанию

Spring Security предоставляет несколько реализаций AccessDecisionVoter. Мы будем использовать некоторые из них как часть нашего решения по обеспечению безопасности.

Давайте посмотрим, как и когда голосуют эти реализации избирателей по умолчанию.

AuthenticatedVoter проголосует на основе уровня аутентификации объекта Authentication — специально ищет либо полностью аутентифицированного принципала, аутентификацию с помощью функции «запомнить меня» или, наконец, анонимного.

RoleVoter голосует, если любой из атрибутов конфигурации начинается со строки «ROLE_». Если это так, он будет искать роль в списке GrantedAuthority объекта Authentication.

WebExpressionVoter позволяет нам использовать SpEL (язык выражений Spring) для авторизации запросов с помощью аннотации @PreAuthorize.

Например, если мы используем конфигурацию Java:

@Override
protected void configure(final HttpSecurity http) throws Exception {
    ...
    .antMatchers("/").hasAnyAuthority("ROLE_USER")
    ...
}

Или используем конфигурацию XML — мы можем использовать SpEL внутри тега intercept-url, в теге http:

<http use-expressions="true">
    <intercept-url pattern="/"
      access="hasAuthority('ROLE_USER')"/>
    ...
</http>

3.2. Пользовательская реализация AccessDecisionVoter

Теперь давайте создадим собственный избиратель, реализуя интерфейс AccessDecisionVoter:

public class MinuteBasedVoter implements AccessDecisionVoter {
   ...
}

Первый из трех методов, которые мы должны предоставить, это метод голосования. Метод голосования — самая важная часть пользовательского избирателя, и именно на нем работает наша логика авторизации.

Метод голосования может возвращать три возможных значения:

    ACCESS_GRANTED – избиратель дает утвердительный ответ ACCESS_DENIED – избиратель дает отрицательный ответ ACCESS_ABSTAIN – избиратель воздерживается от голосования

Теперь реализуем метод голосования:

@Override
public int vote(
  Authentication authentication, Object object, Collection collection) {
    return authentication.getAuthorities().stream()
      .map(GrantedAuthority::getAuthority)
      .filter(r -> "ROLE_USER".equals(r) 
        && LocalDateTime.now().getMinute() % 2 != 0)
      .findAny()
      .map(s -> ACCESS_DENIED)
      .orElseGet(() -> ACCESS_ABSTAIN);
}

В нашем методе голосования мы проверяем, исходит ли запрос от ПОЛЬЗОВАТЕЛЯ. Если это так, мы возвращаем ACCESS_GRANTED, если это четная минута, в противном случае мы возвращаем ACCESS_DENIED. Если запрос исходит не от ПОЛЬЗОВАТЕЛЯ, мы воздерживаемся от голосования и возвращаем ACCESS_ABSTAIN.

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

@Override
public boolean supports(ConfigAttribute attribute) {
    return true;
}

Третий метод возвращает, может ли избиратель голосовать за защищенный тип объекта или нет. Поскольку наш избиратель не связан с защищенным типом объекта, мы возвращаем true:

@Override
public boolean supports(Class clazz) {
    return true;
}

4. AccessDecisionManager

Окончательное решение об авторизации обрабатывается AccessDecisionManager.

AbstractAccessDecisionManager содержит список AccessDecisionVoters, которые отвечают за подачу своих голосов независимо друг от друга.

Существует три реализации для обработки голосов, чтобы охватить наиболее распространенные варианты использования:

    AffirmativeBased — предоставляет доступ, если какой-либо из AccessDecisionVoters возвращает утвердительный голос. ConsensusBased — предоставляет доступ, если имеется больше голосов, чем отрицательный (игнорирование пользователей, которые воздержались) UnanimousBased — предоставляет доступ, если каждый голосующий либо воздерживается, либо голосует «за»

Конечно, вы можете реализовать свой собственный AccessDecisionManager со своей пользовательской логикой принятия решений.

5. Конфигурация

«В этой части руководства мы рассмотрим методы на основе Java и XML для настройки нашего пользовательского AccessDecisionVoter с помощью AccessDecisionManager.

5.1. Конфигурация Java

Давайте создадим класс конфигурации для Spring Web Security:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}

И давайте определим bean-компонент AccessDecisionManager, который использует диспетчер UnanimousBased с нашим настроенным списком голосующих:

@Bean
public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<? extends Object>> decisionVoters 
      = Arrays.asList(
        new WebExpressionVoter(),
        new RoleVoter(),
        new AuthenticatedVoter(),
        new MinuteBasedVoter());
    return new UnanimousBased(decisionVoters);
}

Наконец, давайте настроим Spring Безопасность для использования ранее определенного bean-компонента в качестве AccessDecisionManager по умолчанию:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    ...
    .anyRequest()
    .authenticated()
    .accessDecisionManager(accessDecisionManager());
}

5.2. Конфигурация XML

При использовании конфигурации XML вам необходимо изменить файл spring-security.xml (или любой другой файл, содержащий ваши настройки безопасности).

Сначала вам нужно изменить тег \u003chttp\u003e:

<http access-decision-manager-ref="accessDecisionManager">
  <intercept-url
    pattern="/**"
    access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')"/>
  ...
</http>

Затем добавьте компонент для пользовательского избирателя:

<beans:bean
  id="minuteBasedVoter"
  class="com.baeldung.voter.MinuteBasedVoter"/>

Затем добавьте компонент для AccessDecisionManager:

<beans:bean 
  id="accessDecisionManager" 
  class="org.springframework.security.access.vote.UnanimousBased">
    <beans:constructor-arg>
        <beans:list>
            <beans:bean class=
              "org.springframework.security.web.access.expression.WebExpressionVoter"/>
            <beans:bean class=
              "org.springframework.security.access.vote.AuthenticatedVoter"/>
            <beans:bean class=
              "org.springframework.security.access.vote.RoleVoter"/>
            <beans:bean class=
              "com.baeldung.voter.MinuteBasedVoter"/>
        </beans:list>
    </beans:constructor-arg>
</beans:bean>

Вот пример тега \u003cauthentication-manager\u003e, поддерживающий наш сценарий:

<authentication-manager>
    <authentication-provider>
        <user-service>
            <user name="user" password="pass" authorities="ROLE_USER"/>
            <user name="admin" password="pass" authorities="ROLE_ADMIN"/>
        </user-service>
    </authentication-provider>
</authentication-manager>

Если вы используете комбинацию конфигурации Java и XML, вы можете импортировать XML в класс конфигурации:

@Configuration
@ImportResource({"classpath:spring-security.xml"})
public class XmlSecurityConfig {
    public XmlSecurityConfig() {
        super();
    }
}

6. Заключение

В этом руководстве мы рассмотрели способ настройки безопасности для веб-приложения Spring с помощью AccessDecisionVoters. Мы видели некоторых избирателей, предоставленных Spring Security, которые внесли свой вклад в наше решение. Затем мы обсудили, как реализовать собственный AccessDecisionVoter.

Затем мы обсудили, как AccessDecisionManager принимает окончательное решение об авторизации, и показали, как использовать реализации, предоставляемые Spring, для принятия этого решения после того, как все избиратели отдали свои голоса.

Затем мы настроили список AccessDecisionVoters с AccessDecisionManager через Java и XML.

Реализацию можно найти в проекте Github.

Когда проект выполняется локально, страница входа доступна по адресу:

http://localhost:8082/login

Учетные данные для ПОЛЬЗОВАТЕЛЯ — «user» и «pass», а учетные данные для АДМИНИСТРАТОРА — «admin» и — «проходить».