Пользовательские AccessDecisionVoters в Spring Security

1. Вступление

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

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

2. Сценарий

Чтобы продемонстрировать, как работает AccessDecisionVoter , мы реализуем сценарий с двумя пользовательскими типами, USER и ADMIN, в котором USER может обращаться к системе только в четные минуты, а ADMIN всегда будет предоставлен доступ.

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

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

3.1. Стандартные AccessDecisionVoter Реализации

Spring Security предоставляет несколько реализаций AccessDecisionVoter .

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

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

AuthenticatedVoter проголосует на основе уровня аутентификации объекта Authentication , в частности, для поиска полностью аутентифицированного тарифа, аутентифицированного с помощью Remember-Me или, наконец, анонимного.

RoleVoter голосует, если любой из атрибутов конфигурации начинается со строки __ «ROLE».

WebExpressionVoter позволяет нам использовать SpEL (Spring Expression Language) для авторизации запросов с использованием аннотации @ 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 {
   ...
}

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

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

  • ACCESS GRANTED__ - избиратель дает утвердительный ответ

  • ACCESS DENIED__ - избиратель дает отрицательный ответ

  • ACCESS ABSTAIN__ - избиратель воздерживается от голосования

Давайте теперь реализуем метод vote :

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

В нашем методе vote мы проверяем, поступил ли запрос от USER . Если это так, мы возвращаем ACCESS GRANTED , если это четная минута, в противном случае мы возвращаем ACCESS DENIED. Если запрос не поступает от USER, мы воздерживаемся от голосования и возвращаем ACCESS ABSTAIN__.

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

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

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

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

4. AccessDecisionManager

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

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

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

  • AffirrativeBased - предоставляет доступ, если любой из

_AccessDecisionVoter s вернуть голос "за" ** 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 Security для использования ранее определенного bean-компонента в качестве AccessDecisionManager по умолчанию:

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

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

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

Во-первых, вам нужно изменить тег <http> :

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

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

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

Затем добавьте bean-компонент для 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=
              "org.baeldung.voter.MinuteBasedVoter"/>
        </beans:list>
    </beans:constructor-arg>
</beans:bean>

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

<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 с помощью _AccessDecisionVoter s. Мы видели некоторых избирателей, предоставленных Spring Security, которые внесли свой вклад в наше решение. Затем мы обсудили, как реализовать пользовательский AccessDecisionVoter_ .

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

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

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

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

http://localhost:8080/spring-security-custom-permissions/login

Учетные данные для USER - это «пользователь» и «пароль», а учетные данные для « ADMIN » - «администратор» и «пароль».