Перейти к основному содержимому

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

· 5 мин. чтения

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 внутри тега перехвата 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 содержит список AccessDecisionVoter , которые отвечают за подачу своих голосов независимо друг от друга.

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

  • AffirmativeBased — предоставляет доступ, если какой-либо из AccessDecisionVoter возвращает положительный голос.
  • 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>

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

<beans:bean
id="minuteBasedVoter"
class="com.foreach.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=
"com.foreach.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:8082/login

Учетные данные для ПОЛЬЗОВАТЕЛЯ — «user» и «pass», а учетные данные для ADMIN — «admin» и «pass».