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».