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(Listusernames) { return usernames.stream().collect(Collectors.joining(";")); }
В этом примере мы объединяем все имена пользователей, за исключением того, кто прошел проверку подлинности.
Здесь, в нашем выражении, мы используем имя объекта фильтра для представления текущего объекта в коллекции .
Однако, если метод имеет более одного аргумента, который является типом коллекции, нам необходимо использовать свойство filter Target , чтобы указать, какой аргумент мы хотим отфильтровать:
@PreFilter (value = "filterObject != authentication.principal.username", filterTarget = "usernames") public String joinUsernamesAndRoles( Listusernames, List roles) { return usernames.stream().collect(Collectors.joining(";")) + ":" + roles.stream().collect(Collectors.joining(";")); }
Кроме того, мы также можем отфильтровать возвращаемую коллекцию метода с помощью @PostFilter аннотации :
@PostFilter("filterObject != authentication.principal.username") public ListgetAllUsernamesExceptCurrent() { 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 .