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( Listusernames, 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 .