Моя компания изучает Spring MVC, чтобы определить, следует ли использовать его в одном из наших следующих проектов. Пока мне нравится то, что я видел, и прямо сейчас я смотрю на модуль Spring Security, чтобы определить, можем ли мы / должны ли это использовать.
Наши требования безопасности довольно простые; пользователю просто нужно иметь возможность указать имя пользователя и пароль, чтобы иметь доступ к определенным частям сайта (например, для получения информации о своей учетной записи); и на сайте есть несколько страниц (часто задаваемые вопросы, поддержка и т. д.), к которым должен быть предоставлен доступ анонимному пользователю.
В прототипе, который я создавал, я хранил объект «LoginCredentials» (который просто содержит имя пользователя и пароль) в Session для аутентифицированного пользователя; некоторые контроллеры проверяют, находится ли этот объект в сеансе, чтобы, например, получить ссылку на имя пользователя, вошедшего в систему. Я хочу заменить эту самодельную логику на Spring Security, которая имела бы приятное преимущество в виде удаления любого вида «как мы отслеживаем пользователей, вошедших в систему?» и "как мы аутентифицируем пользователей?" из моего контроллера / бизнес-кода.
Похоже, что Spring Security предоставляет объект «контекста» (для каждого потока), чтобы иметь возможность получить доступ к информации об имени пользователя / принципале из любого места в вашем приложении ...
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
... что очень не похоже на Spring, поскольку этот объект в некотором роде (глобальный) синглтон.
Мой вопрос таков: если это стандартный способ доступа к информации об аутентифицированном пользователе в Spring Security, то каков приемлемый способ ввести объект аутентификации в SecurityContext, чтобы он был доступен для моих модульных тестов, когда модульные тесты требуют аутентифицированный пользователь?
Нужно ли мне подключать это к методу инициализации каждого тестового примера?
protected void setUp() throws Exception {
...
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
...
}
Это кажется излишне многословным. Есть способ попроще?
Сам SecurityContextHolder
объект кажется очень не похожим на Spring ...
Просто сделайте это обычным способом, а затем вставьте его, используя
SecurityContextHolder.setContext()
в своем тестовом классе, например:Контроллер:
Контрольная работа:
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);
источник
Authentication a
нужно добавить в контроллер? Как я могу понять в каждом вызове метода? Можно ли для "весеннего пути" просто добавить его вместо инъекции?@BeforeEach
(JUnit5) или@Before
(JUnit 4). Хорошо и просто.Не отвечая на вопрос о том, как создавать и внедрять объекты аутентификации, Spring Security 4.0 предоставляет несколько полезных альтернатив, когда дело доходит до тестирования.
@WithMockUser
Аннотаций позволяет разработчику указывать макет пользователя (с дополнительными органами, имя пользователя, пароль и роли) в опрятном виде:@Test @WithMockUser(username = "admin", authorities = { "ADMIN", "USER" }) public void getMessageWithMockUserCustomAuthorities() { String message = messageService.getMessage(); ... }
Существует также возможность использовать
@WithUserDetails
для эмуляцииUserDetails
возвращенногоUserDetailsService
, например@Test @WithUserDetails("customUsername") public void getMessageWithUserDetailsCustomUsername() { String message = messageService.getMessage(); ... }
Более подробную информацию можно найти в главах @WithMockUser и @WithUserDetails справочной документации Spring Security (из которой были скопированы приведенные выше примеры)
источник
Вы совершенно правы - вызовы статических методов особенно проблематичны для модульного тестирования, поскольку вы не можете легко имитировать свои зависимости. Я собираюсь показать вам, как позволить контейнеру Spring IoC делать за вас грязную работу, оставляя вам аккуратный, проверяемый код. SecurityContextHolder - это класс фреймворка, и хотя для вашего кода безопасности низкого уровня может быть нормально быть привязанным к нему, вы, вероятно, захотите предоставить более аккуратный интерфейс для ваших компонентов пользовательского интерфейса (то есть контроллеров).
cliff.meyers упомянул один способ обойти это - создать свой собственный «основной» тип и внедрить экземпляр в потребителей. Тег Spring < aop: scoped-proxy />, представленный в 2.x, в сочетании с определением bean-компонента области запроса, и поддержка фабричных методов могут быть билетом к наиболее читаемому коду.
Это может работать следующим образом:
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-компонента определите bean-компонент с областью запроса для хранения принципала:
<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); }
Надеюсь это поможет!
источник
Лично я бы просто использовал 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
только в конфигурации Spring, чтобы получитьSecurityContext
источник
Генеральная
Между тем (начиная с версии 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()
ввести пользователя следующим образом:mockMvc.perform(get("/somewhere").with(user(myUserDetails)));
Однако это просто заполнит SecurityContext напрямую. Если вы хотите убедиться, что пользователь загружен из сеанса в вашем тесте, вы можете использовать это:
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; } }; }
источник
Я хотел бы взглянуть на абстрактные классы тестов Спринга и фиктивные объекты , которые говорили здесь . Они предоставляют мощный способ автоматического подключения ваших управляемых объектов Spring, упрощая модульное и интеграционное тестирование.
источник
Аутентификация - это свойство потока в серверной среде точно так же, как это свойство процесса в ОС. Наличие экземпляра компонента для доступа к информации аутентификации было бы неудобной конфигурацией и накладными расходами без каких-либо преимуществ.
Что касается тестовой аутентификации, есть несколько способов облегчить себе жизнь. Я предпочитаю создавать собственные аннотации
@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 }
источник