Испытание блока с безопасностью весны
моя компания оценивает Spring MVC, чтобы определить, следует ли использовать его в одном из наших следующих проектов. До сих пор мне нравится то, что я видел, и прямо сейчас я смотрю на модуль безопасности Spring, чтобы определить, можно ли его использовать.
наши требования к безопасности довольно просты, пользователь просто должен ввести имя пользователя и пароль, чтобы иметь возможность получить доступ к определенной части сайта (например, чтобы получить информацию о своем счете); и есть несколько страниц на сайте (Часто задаваемые вопросы, поддержка и т. д.), где анонимный пользователь должен получить доступ.
в прототипе, который я создавал, я хранил объект "LoginCredentials" (который просто содержит имя пользователя и пароль) в сеансе для аутентифицированного пользователя; некоторые контроллеры проверяют, находится ли этот объект в сеансе, чтобы получить ссылку на имя пользователя, вошедшего в систему, например. Я ищу, чтобы заменить эту доморощенную логику на Spring Security вместо этого, которая будет иметь хорошее преимущество удаления любого вида "как мы отслеживаем зарегистрированных пользователей?"и "как мы проверки подлинности пользователей?"из моего контроллера / бизнес-кода.
похоже, что Spring Security предоставляет объект "контекст" (для каждого потока), чтобы иметь возможность получить доступ к имени пользователя/основной информации из любого места вашего приложения...
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
... что кажется очень не-весенним, поскольку этот объект является (глобальным) синглтоном, в некотором роде.
мой вопрос таков: если это стандартный способ доступа к сведения об аутентифицированном пользователе в Spring Security, каков принятый способ внедрения объекта аутентификации в SecurityContext, чтобы он был доступен для моих модульных тестов, когда модульные тесты требуют аутентифицированного пользователя?
мне нужно подключить это в методе инициализации каждого тестового случая?
protected void setUp() throws Exception {
...
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
...
}
это кажется слишком многословным. Есть ли более простой способ?
The SecurityContextHolder сам объект, кажется, очень не по-весеннему...
11 ответов:
проблема в том, что Spring Security не делает объект аутентификации доступным как Боб в контейнере, поэтому нет способа легко ввести или автоматически подключить его из коробки.
прежде чем мы начнем использовать Spring Security, мы создадим компонент с областью действия сеанса в контейнере для хранения участника, введем его в "AuthenticationService" (singleton), а затем введем этот компонент в другие службы, которые нуждаются в знаниях текущего участника.
Если вы реализуете свою собственную службу аутентификации, вы можете в основном сделать то же самое: создать компонент с областью сеанса со свойством "principal", внедрить его в свою службу аутентификации, настроить службу auth на успешную проверку подлинности, а затем сделать службу auth доступной для других компонентов по мере необходимости.
Я бы не чувствовал себя слишком плохо об использовании SecurityContextHolder. хотя. Я знаю, что это статический / Синглтон и что весна не поощряет использование таких вещей но их реализация заботится о том, чтобы вести себя соответствующим образом в зависимости от среды: session-scoped в контейнере сервлета, thread-scoped в тесте JUnit и т. д. Реальный ограничивающий фактор Синглтона - это когда он обеспечивает реализацию, которая негибка для разных сред.
просто сделайте это обычным способом, а затем вставьте его с помощью
:SecurityContextHolder.setContext()в вашем тестовом классе, например:Authentication a = SecurityContextHolder.getContext().getAuthentication();тест:
Authentication authentication = Mockito.mock(Authentication.class); // Mockito.whens() for your authorization object SecurityContext securityContext = Mockito.mock(SecurityContext.class); Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext);
вы совершенно правы, чтобы быть обеспокоены-статические вызовы методов особенно проблематичны для модульного тестирования, поскольку вы не можете легко издеваться над своими зависимостями. Я собираюсь показать вам, как позволить контейнеру Spring IoC сделать за вас грязную работу, оставив вас с аккуратным, тестируемым кодом. SecurityContextHolder-это класс фреймворка, и хотя ваш низкоуровневый код безопасности может быть привязан к нему, вы, вероятно, захотите предоставить более аккуратный интерфейс вашим компонентам пользовательского интерфейса (т. е. контроллеры.)
скалы.Мейерс упомянул один из способов обойти это-создать свой собственный "основной" тип и внедрить экземпляр в потребителей. Весна aop: scoped-proxy тег/> введен в 2.x в сочетании с определением Бина области запроса и поддержкой фабричного метода может быть билетом к наиболее читаемому коду.
Это может работать следующим образом:
public class MyUserDetails implements UserDetails { // this is your custom UserDetails implementation to serve as a principal // implement the Spring methods and add your own methods as appropriate } public class MyUserHolder { public static MyUserDetails getUserDetails() { Authentication a = SecurityContextHolder.getContext().getAuthentication(); if (a == null) { return null; } else { return (MyUserDetails) a.getPrincipal(); } } } public class MyUserAwareController { MyUserDetails currentUser; public void setCurrentUser(MyUserDetails currentUser) { this.currentUser = currentUser; } // controller code }пока ничего сложного, правда? На самом деле вы, вероятно, должны были сделать большую часть этого уже. Затем в контексте вашего компонента определите компонент с областью запроса для хранения основного:
<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request"> <aop:scoped-proxy/> </bean> <bean id="controller" class="MyUserAwareController"> <property name="currentUser" ref="userDetails"/> <!-- other props --> </bean>благодаря магии тега AOP: scoped-proxy статический метод getUserDetails будет вызываться каждый раз, когда поступает новый HTTP-запрос, и любые ссылки на свойство currentUser будут правильно разрешены. Теперь модульное тестирование становится тривиальным:
protected void setUp() { // existing init code MyUserDetails user = new MyUserDetails(); // set up user as you wish controller.setCurrentUser(user); }надеюсь, что это помогает!
не отвечая на вопрос о том, как создавать и внедрять объекты аутентификации, Spring Security 4.0 предоставляет некоторые приветствуемые альтернативы, когда дело доходит до тестирования. Элемент
@WithMockUserаннотация позволяет разработчику указать макет пользователя (с дополнительными полномочиями, именем пользователя, паролем и ролями) аккуратным способом:@Test @WithMockUser(username = "admin", authorities = { "ADMIN", "USER" }) public void getMessageWithMockUserCustomAuthorities() { String message = messageService.getMessage(); ... }есть также возможность использовать
@WithUserDetailsэмулировать aUserDetailsвернулся изUserDetailsService, например,@Test @WithUserDetails("customUsername") public void getMessageWithUserDetailsCustomUsername() { String message = messageService.getMessage(); ... }больше деталей можно найти внутри элемент @WithMockUser и @WithUserDetails главы в справочных документах Spring Security (из которых были скопированы приведенные выше примеры)
лично я бы просто использовал Powermock вместе с Mockito или Easymock, чтобы издеваться над статическим SecurityContextHolder.getSecurityContext () в вашем модульном/интеграционном тесте, например
@RunWith(PowerMockRunner.class) @PrepareForTest(SecurityContextHolder.class) public class YourTestCase { @Mock SecurityContext mockSecurityContext; @Test public void testMethodThatCallsStaticMethod() { // Set mock behaviour/expectations on the mockSecurityContext when(mockSecurityContext.getAuthentication()).thenReturn(...) ... // Tell mockito to use Powermock to mock the SecurityContextHolder PowerMockito.mockStatic(SecurityContextHolder.class); // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext() Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext); ... } }по общему признанию, здесь есть довольно много кода котельной плиты, т. е. макет объекта аутентификации, макет SecurityContext для возврата аутентификации и, наконец, макет SecurityContextHolder для получения SecurityContext, однако его очень гибкий и позволяет выполнять модульный тест для таких сценариев, как null Объекты проверки подлинности и т. д. без необходимости изменять ваш (не тестовый) код
использование статики в этом случае-лучший способ написать безопасный код.
да, статика вообще плохая-вообще, но в этом случае статика-это то, что вы хотите. Поскольку контекст безопасности связывает участника с текущим потоком, наиболее безопасный код будет обращаться к статическому из потока как можно напрямую. Скрытие доступа за введенным классом-оболочкой предоставляет злоумышленнику больше очков для атаки. Им не нужен был бы доступ к код (который им было бы трудно изменить, если бы jar был подписан), им просто нужен способ переопределить конфигурацию, что можно сделать во время выполнения или проскальзывания некоторого XML на путь к классам. Даже использование инъекции аннотаций будет переопределено с помощью внешнего XML. Такой XML может внедрить в работающую систему неуправляемый Принципал.
Я сам задал тот же вопрос здесь, и только что опубликовал ответ, который я недавно нашел. Короткий ответ: впрысните a
SecurityContext, и относятся кSecurityContextHolderтолько в вашей весенней конфигурации, чтобы получитьSecurityContext
Я бы посмотрел на абстрактные тестовые классы Spring и макетные объекты, о которых говорят здесь. Они обеспечивают мощный путь автоматическ-связывать проволокой ваши объекты управляемые весной делая испытание блока и внедрения более легким.
общие
тем временем (начиная с версии 3.2, в 2013 году, благодаря SEC-2298) аутентификация может быть введена в методы MVC с помощью аннотации @AuthenticationPrincipal:
@Controller class Controller { @RequestMapping("/somewhere") public void doStuff(@AuthenticationPrincipal UserDetails myUser) { } }тесты
в вашем модульном тесте вы, очевидно, можете вызвать этот метод напрямую. В интеграционных тестах используется
org.springframework.test.web.servlet.MockMvcможно использоватьorg.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user()чтобы ввести пользователя следующим образом:это будет просто непосредственно заполните SecurityContext. Если вы хотите убедиться, что пользователь загружен из сеанса в тесте, вы можете использовать это:mockMvc.perform(get("/somewhere").with(user(myUserDetails)));mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails))); /* ... */ private static RequestPostProcessor sessionUser(final UserDetails userDetails) { return new RequestPostProcessor() { @Override public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) { final SecurityContext securityContext = new SecurityContextImpl(); securityContext.setAuthentication( new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) ); request.getSession().setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext ); return request; } }; }
аутентификация-это свойство потока в серверной среде так же, как и свойство процесса в ОС. Наличие экземпляра bean для доступа к информации аутентификации было бы неудобной конфигурацией и проводкой накладных расходов без какой-либо выгоды.
Что касается тестовой аутентификации есть несколько способов, как вы можете сделать вашу жизнь проще. Мой любимый-сделать пользовательскую аннотацию
@Authenticatedи прослушиватель выполнения теста, который управляет им. ПроверьтеDirtiesContextTestExecutionListenerдля вдохновение.
после довольно большой работы мне удалось воспроизвести желаемое поведение. Я эмулировал логин через MockMvc. Это слишком тяжело для большинства модульных тестов, но полезно для интеграционных тестов.
конечно, я готов увидеть эти новые функции в Spring Security 4.0, которые облегчат наше тестирование.
package [myPackage] import static org.junit.Assert.*; import javax.inject.Inject; import javax.servlet.http.HttpSession; import org.junit.Before; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @ContextConfiguration(locations={[my config file locations]}) @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) public static class getUserConfigurationTester{ private MockMvc mockMvc; @Autowired private FilterChainProxy springSecurityFilterChain; @Autowired private MockHttpServletRequest request; @Autowired private WebApplicationContext webappContext; @Before public void init() { mockMvc = MockMvcBuilders.webAppContextSetup(webappContext) .addFilters(springSecurityFilterChain) .build(); } @Test public void testTwoReads() throws Exception{ HttpSession session = mockMvc.perform(post("/j_spring_security_check") .param("j_username", "admin_001") .param("j_password", "secret007")) .andDo(print()) .andExpect(status().isMovedTemporarily()) .andExpect(redirectedUrl("/index")) .andReturn() .getRequest() .getSession(); request.setSession(session); SecurityContext securityContext = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); SecurityContextHolder.setContext(securityContext); // Your test goes here. User is logged with }
Comments