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 » - «администратор» и «пароль».