Рубрики
Без рубрики

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

Руководство по безопасности на уровне методов с использованием Spring Security framework.

Автор оригинала: baeldung.

1. введение

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

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

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

Дальнейшее чтение:

Руководство по языку выражений Spring

Пользовательское выражение безопасности с помощью Spring Security

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

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


    org.springframework.security
    spring-security-config

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

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


    org.springframework.boot
    spring-boot-starter-security

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

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

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

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

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

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

Аннотация @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 .

То @Обеспечено аннотация не поддерживает язык выражений 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 может выполнить GetUserName 2 .

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

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

Как @PreAuthorize , так и @PostAuthorize аннотации обеспечивают управление доступом на основе выражений. Следовательно, предикаты могут быть написаны с использованием SpEL (язык выражений Spring) .

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

Теперь давайте объявим get Username В верхнем регистре метод, как показано ниже:

@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 только в том случае, если значение аргумента username совпадает с именем пользователя текущего участника.

Стоит отметить, что выражения @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);
}

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

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

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

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

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

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

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

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

@PreFilter
  (value = "filterObject != authentication.principal.username",
  filterTarget = "usernames")
public String joinUsernamesAndRoles(
  List usernames, List roles) {
 
    return usernames.stream().collect(Collectors.joining(";")) 
      + ":" + roles.stream().collect(Collectors.joining(";"));
}

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

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

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

При такой конфигурации 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(){
        //...
    }
}

В приведенном выше примере правило безопасности имеет роль(‘ROLE_ADMIN’) будет применяться как к методам get System Year , так и к методам |/getSystemDate .

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

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

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

Следовательно, Spring проверит авторизацию как до, так и после выполнения метода secure Load User Detail .

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

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

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

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

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

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


    org.springframework.security
    spring-security-test

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

Далее, давайте настроим простой тест интеграции Spring, указав runner и конфигурацию 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 . Если мы не указываем имя пользователя или роль , по умолчанию имя пользователя является пользователем и по умолчанию роль является ROLE_USER .

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

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

Например, давайте объявим get Username В нижнем регистре метод:

@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. UserDetails интерфейс.

В этой статье мы объявляем класс Custom User , который расширяет существующую реализацию 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);
}

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

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

@Test
@WithUserDetails(
  value = "john", 
  userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {
 
    CustomUser user = userService.loadUserDetail("jane");

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

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

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

Удобно, что мы также можем украсить @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. Заключение

В этом уроке мы рассмотрели различные варианты использования безопасности методов в Spring Security.

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

Все примеры этого руководства можно найти на Github .