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

Введение в безопасность метода Spring

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

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

1. Обзор

Проще говоря, Spring Security поддерживает семантику авторизации на уровне метода.

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

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

2. Включение безопасности метода

Во-первых, чтобы использовать Spring Method Security, нам нужно добавить зависимость spring-security-config :

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>

Его последнюю версию мы можем найти на Maven Central .

Если мы хотим использовать Spring Boot, мы можем использовать зависимость spring-boot-starter-security , которая включает spring-security-config :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Опять же, последнюю версию можно найти на Maven Central .

Далее нам нужно включить глобальную безопасность методов :

@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig
extends GlobalMethodSecurityConfiguration {
}
  • Свойство prePostEnabled включает аннотации Spring Security до/после.
  • Свойство secureEnabled определяет, следует ли включить аннотацию @Secured .
  • Свойство jsr250Enabled позволяет нам использовать аннотацию @RoleAllowed .

Подробнее об этих аннотациях мы поговорим в следующем разделе.

3. Применение безопасности метода

3.1. Использование аннотации @Secured

Аннотация @Secured используется для указания списка ролей в методе. Таким образом, пользователь может получить доступ к этому методу только в том случае, если у него есть хотя бы одна из указанных ролей.

Давайте определим метод getUsername :

@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}

Здесь аннотация @Secured("ROLE_VIEWER") определяет, что только пользователи с ролью ROLE_VIEWER могут выполнять метод getUsername .

Кроме того, мы можем определить список ролей в аннотации @Secured :

@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
return userRoleRepository.isValidUsername(username);
}

В этом случае в конфигурации указано, что если у пользователя есть ROLE_VIEWER или ROLE_EDITOR , этот пользователь может вызвать метод isValidUsername .

Аннотация @Secured не поддерживает язык выражений Spring (SpEL).

3.2. Использование аннотации @RolesAllowed

Аннотация @RolesAllowed является эквивалентной аннотацией JSR-250 аннотации @Secured .

По сути, мы можем использовать аннотацию @RolesAllowed аналогично @Secured .

Таким образом, мы могли бы переопределить методы getUsername и isValidUsername :

@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
//...
}

@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
//...
}

Точно так же только пользователь с ролью ROLE_VIEWER может выполнить getUsername2 .

Опять же, пользователь может вызвать isValidUsername2 только в том случае, если у него есть хотя бы одна из ролей ROLE_VIEWER или ROLER_EDITOR .

3.3. Использование аннотаций @PreAuthorize и @PostAuthorize

Аннотации @PreAuthorize и @PostAuthorize обеспечивают управление доступом на основе выражений. Итак, предикаты можно писать с помощью SpEL (Spring Expression Language) .

Аннотация @PreAuthorize проверяет данное выражение перед входом в метод , тогда как аннотация @PostAuthorize проверяет его после выполнения метода и может изменить результат.

Теперь давайте объявим метод getUsernameInUpperCase , как показано ниже:

@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
return getUsername().toUpperCase();
}

@PreAuthorize ("hasRole('ROLE_VIEWER')") имеет то же значение, что и @Secured("ROLE_VIEWER") , которое мы использовали в предыдущем разделе. Не стесняйтесь узнавать больше подробностей о выражениях безопасности в предыдущих статьях .

Следовательно, аннотацию @Secured({"ROLE_VIEWER", "ROLE_EDITOR"}) можно заменить на @PreAuthorize("hasRole('ROLE_VIEWER') или hasRole('ROLE_EDITOR')") :

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
//...
}

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

@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
//...
}

Здесь пользователь может вызывать метод getMyRoles только в том случае, если значение аргумента имя пользователя совпадает с именем пользователя текущего принципала.

Стоит отметить, что выражения @PreAuthorize можно заменить выражениями @PostAuthorize .

Давайте перепишем getMyRoles :

@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
//...
}

Однако в предыдущем примере авторизация задерживалась после выполнения целевого метода.

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

@PostAuthorize
("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}

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

В этом разделе мы в основном используем простые выражения Spring. Для более сложных сценариев мы можем создавать собственные выражения безопасности .

3.4. Использование аннотаций @PreFilter и @PostFilter

Spring Security предоставляет аннотацию @PreFilter для фильтрации аргумента коллекции перед выполнением метода :

@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List<String> usernames) {
return usernames.stream().collect(Collectors.joining(";"));
}

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

Здесь в нашем выражении мы используем имя filterObject для представления текущего объекта в коллекции.

Однако, если метод имеет более одного аргумента, который является типом коллекции, нам нужно использовать свойство filterTarget , чтобы указать, какой аргумент мы хотим отфильтровать:

@PreFilter
(value = "filterObject != authentication.principal.username",
filterTarget = "usernames")
public String joinUsernamesAndRoles(
List<String> usernames, List<String> roles) {

return usernames.stream().collect(Collectors.joining(";"))
+ ":" + roles.stream().collect(Collectors.joining(";"));
}

Кроме того, мы также можем отфильтровать возвращаемую коллекцию метода с помощью аннотации @PostFilter :

@PostFilter("filterObject != authentication.principal.username")
public List<String> getAllUsernamesExceptCurrent() {
return userRoleRepository.getAllUsernames();
}

В этом случае имя filterObject относится к текущему объекту в возвращаемой коллекции.

С этой конфигурацией Spring Security будет перебирать возвращаемый список и удалять все значения, соответствующие имени пользователя принципала.

В нашей статье Spring Security — @PreFilter и @PostFilter обе аннотации описаны более подробно.

3.5. Метааннотация безопасности метода

Обычно мы оказываемся в ситуации, когда мы защищаем разные методы, используя одну и ту же конфигурацию безопасности.

В этом случае мы можем определить мета-аннотацию безопасности:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}

Затем мы можем напрямую использовать аннотацию @IsViewer для защиты нашего метода:

@IsViewer
public String getUsername4() {
//...
}

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

3.6. Аннотации безопасности на уровне класса

Если мы обнаружим, что используем одну и ту же аннотацию безопасности для каждого метода в одном классе, мы можем рассмотреть возможность размещения этой аннотации на уровне класса:

@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {

public String getSystemYear(){
//...
}

public String getSystemDate(){
//...
}
}

В приведенном выше примере правило безопасности hasRole('ROLE_ADMIN') будет применяться как к методам getSystemYear , так и к методам getSystemDate .

3.7. Несколько аннотаций безопасности для метода

Мы также можем использовать несколько аннотаций безопасности в одном методе:

@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}

Таким образом, Spring проверит авторизацию как до, так и после выполнения метода secureLoadUserDetail .

4. Важные соображения

Есть два момента, которые мы хотели бы напомнить в отношении безопасности методов:

  • По умолчанию проксирование Spring AOP используется для применения безопасности метода. Если защищенный метод A вызывается другим методом в том же классе, безопасность в A полностью игнорируется. Это означает, что метод A будет выполняться без какой-либо проверки безопасности. То же самое относится и к частным методам.
  • Spring SecurityContext привязан к потоку. По умолчанию контекст безопасности не распространяется на дочерние потоки. Для получения дополнительной информации обратитесь к нашей статье Распространение контекста безопасности Spring .

5. Безопасность метода тестирования

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

Чтобы протестировать Spring Security с помощью JUnit, нам нужна зависимость spring-security-test :

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>

Нам не нужно указывать версию зависимости, потому что мы используем плагин Spring Boot. Мы можем найти последнюю версию этой зависимости на Maven Central .

Затем давайте настроим простой тест Spring Integration, указав бегун и конфигурацию ApplicationContext :

@RunWith(SpringRunner.class)
@ContextConfiguration
public class MethodSecurityIntegrationTest {
// ...
}

5.2. Тестирование имени пользователя и ролей

Теперь, когда наша конфигурация готова, давайте попробуем протестировать наш метод getUsername , который мы защитили с помощью аннотации @Secured("ROLE_VIEWER") :

@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}

Поскольку здесь мы используем аннотацию @Secured , для вызова метода требуется, чтобы пользователь прошел аутентификацию. В противном случае мы получим исключение AuthenticationCredentialsNotFoundException .

Итак, нам нужно предоставить пользователя для тестирования нашего защищенного метода.

Для этого мы украшаем тестовый метод @WithMockUser и предоставляем пользователя и роли :

@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
String userName = userRoleService.getUsername();

assertEquals("john", userName);
}

Мы предоставили аутентифицированного пользователя с именем пользователя john и ролью ROLE_VIEWER . Если мы не указываем имя пользователя или роль , имя пользователя по умолчанию user, а роль по умолчанию — ROLE_USER .

Обратите внимание, что здесь нет необходимости добавлять префикс ROLE_ , потому что Spring Security добавит этот префикс автоматически.

Если мы не хотим иметь этот префикс, мы можем рассмотреть возможность использования полномочий вместо роли .

Например, давайте объявим метод getUsernameInLowerCase :

@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
return getUsername().toLowerCase();
}

Мы могли бы проверить это, используя авторитеты:

@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
String username = userRoleService.getUsernameInLowerCase();

assertEquals("john", username);
}

Удобно, если мы хотим использовать одного и того же пользователя для многих тестовых случаев, мы можем объявить аннотацию @WithMockUser в тестовом классе :

@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class MockUserAtClassLevelIntegrationTest {
//...
}

Если бы мы хотели запустить наш тест как анонимный пользователь, мы могли бы использовать аннотацию @WithAnonymousUser :

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
userRoleService.getUsername();
}

В приведенном выше примере мы ожидаем возникновения AccessDeniedException , поскольку анонимному пользователю не предоставлена роль ROLE_VIEWER или полномочия SYS_ADMIN .

5.3. Тестирование с пользовательским сервисом UserDetailsService

Для большинства приложений в качестве принципала проверки подлинности обычно используется пользовательский класс. В этом случае пользовательский класс должен реализовать org.springframework.security.core.userdetails. Интерфейс сведений о пользователе .

В этой статье мы объявляем класс CustomUser , который расширяет существующую реализацию UserDetails , то есть org.springframework.security.core.userdetails. Пользователь :

public class CustomUser extends User {
private String nickName;
// getter and setter
}

Вернемся к примеру с аннотацией @PostAuthorize в разделе 3:

@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}

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

Если бы мы хотели протестировать этот метод, мы могли бы предоставить реализацию UserDetailsService , которая могла бы загружать нашего CustomUser на основе имени пользователя :

@Test
@WithUserDetails(
value = "john",
userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {

CustomUser user = userService.loadUserDetail("jane");

assertEquals("jane", user.getNickName());
}

Здесь аннотация @WithUserDetails указывает, что мы будем использовать UserDetailsService для инициализации нашего аутентифицированного пользователя. На службу ссылается свойство userDetailsServiceBeanName . Этот UserDetailsService может быть реальной реализацией или подделкой для целей тестирования.

Кроме того, служба будет использовать значение свойства value в качестве имени пользователя для загрузки UserDetails .

Для удобства мы также можем украсить аннотацией @WithUserDetails на уровне класса, аналогично тому, что мы сделали с аннотацией @WithMockUser .

5.4. Тестирование с помощью мета-аннотаций

Мы часто обнаруживаем, что снова и снова используем одних и тех же пользователей/роли в различных тестах.

Для таких ситуаций удобно создать мета-аннотацию .

Снова взглянув на предыдущий пример @WithMockUser(username="john", roles={"VIEWER"}) , мы можем объявить мета-аннотацию:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }

Затем мы можем просто использовать @WithMockJohnViewer в нашем тесте:

@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
String userName = userRoleService.getUsername();

assertEquals("john", userName);
}

Точно так же мы можем использовать мета-аннотации для создания доменных пользователей с помощью @WithUserDetails .

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

В этой статье мы рассмотрели различные варианты использования Method Security в Spring Security.

Мы также рассмотрели несколько методов, позволяющих легко протестировать безопасность методов, и узнали, как повторно использовать фиктивных пользователей в различных тестах.

Все примеры для этой статьи можно найти на GitHub .