Модульное тестирование с Spring Security

Моя компания проводила оценку Spring MVC, чтобы определить, следует ли нам использовать его в одном из наших следующих проектов. Пока что мне нравится то, что я видел, и сейчас я смотрю на модуль Spring Security, чтобы определить, можем ли мы / должны это использовать.

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

В создаваемом мной прототипе я хранил объект «LoginCredentials» (который просто содержит имя пользователя и пароль) в сеансе для аутентифицированного пользователя; некоторые контроллеры проверяют, находится ли этот объект в сеансе, например, для получения ссылки на имя пользователя, вошедшего в систему. Вместо этого я собираюсь заменить эту самодельную логику на Spring Security, что было бы неплохо, если бы вы удалили «как мы отслеживаем зарегистрированных пользователей?». и "как мы аутентифицируем пользователей?" из моего контроллера / бизнес-код.

Кажется, что Spring Security предоставляет (для каждого потока) «контекстный» объект, чтобы иметь возможность доступа к имени пользователя / основной информации из любой точки вашего приложения ...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

... что выглядит очень не по-весеннему, так как этот объект в некотором смысле является (глобальным) синглтоном.

Мой вопрос заключается в следующем: если это стандартный способ доступа к информации о прошедшем проверку подлинности пользователя в Spring Security, каков приемлемый способ добавить объект Authentication в SecurityContext, чтобы он был доступен для моих модульных тестов, когда для модульных тестов требуется аутентифицированный пользователь?

Нужно ли подключать это в методе инициализации каждого теста?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

Это кажется слишком многословным. Есть ли более простой способ?

Сам SecurityContextHolderобъект выглядит очень не по-весеннему ...

11.12.2008 19:11:43
11 ОТВЕТОВ
РЕШЕНИЕ

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

Прежде чем мы начали использовать Spring Security, мы должны создать bean-объект с сессионной областью в контейнере для хранения принципала, внедрить его в «AuthenticationService» (singleton) и затем внедрить этот bean-компонент в другие службы, которым требовалось знание текущего принципала.

Если вы реализуете свою собственную службу аутентификации, вы в основном можете сделать то же самое: создать сессионный компонент с свойством «принципал», внедрить его в вашу службу аутентификации, сделать так, чтобы служба аутентификации установила свойство для успешной аутентификации, а затем сделайте службу аутентификации доступной для других компонентов по мере необходимости.

Я не чувствую себя слишком плохо об использовании SecurityContextHolder. хоть. Я знаю, что это статический / Singleton, и что Spring не рекомендует использовать такие вещи, но их реализация заботится о том, чтобы вести себя соответствующим образом в зависимости от среды: сессионная область в контейнере сервлета, потоковая область в тесте JUnit и т. Д. Реальный ограничивающий фактор Синглтона - это когда он обеспечивает реализацию, которая негибка для различных сред.

46
16.12.2008 19:27:37
Спасибо, это полезный совет. До сих пор я в основном продолжал вызывать SecurityContextHolder.getContext () (через несколько моих собственных методов-оболочек, так что, по крайней мере, он вызывается только из одного класса).
matt b 16.12.2008 19:56:39
Хотя только одна заметка - я не думаю, что ServletContextHolder имеет какое-либо понятие HttpSession или способ узнать, работает ли он в среде веб-сервера - он использует ThreadLocal, если вы не настроите его для использования чего-то другого (единственными двумя другими встроенными режимами являются InheritableThreadLocal и Global)
matt b 16.12.2008 19:57:19
Единственный недостаток использования bean-компонентов в сеансе / запросе в Spring заключается в том, что они не пройдут тест JUnit. То, что вы можете сделать, это реализовать пользовательскую область, которая будет использовать сеанс / запрос, если он доступен, и откат к потоку необходим. Я предполагаю, что Spring Security делает нечто подобное ...
cliff.meyers 16.12.2008 21:30:07
Моя цель - построить API для отдыха без сессий. Возможно, с обновляемым токеном. Хотя это не ответило на мой вопрос, это помогло. Спасибо
Pomagranite 16.08.2017 14:12:31

Я хотел бы взглянуть на абстрактные классы тестов Спринга и фиктивные объекты , которые говорили здесь . Они предоставляют мощный способ автоматического подключения управляемых объектов Spring, облегчая тестирование модулей и интеграцию.

2
11.12.2008 19:30:58
Хотя эти тестовые занятия полезны, я не уверен, применимы ли они здесь. Мои тесты не имеют понятия ApplicationContext - они не нужны. Все, что мне нужно, это убедиться, что SecurityContext заполнен перед запуском тестового метода - просто кажется грязным, чтобы сначала установить его в ThreadLocal
matt b 11.12.2008 19:31:59

Вы совершенно правы, что обеспокоены - вызовы статических методов особенно проблематичны для модульного тестирования, поскольку вы не можете легко смоделировать свои зависимости. Я собираюсь показать вам, как позволить контейнеру Spring IoC выполнять грязную работу за вас, оставляя вам аккуратный тестируемый код. SecurityContextHolder - это каркасный класс, и, хотя ваш низкоуровневый код безопасности может быть привязан к нему, возможно, вы захотите предоставить более удобный интерфейс для ваших компонентов пользовательского интерфейса (т. Е. Контроллеров).

cliff.meyers упомянул об одном способе - создать свой собственный «основной» тип и внедрить экземпляр в потребителей. Тег Spring < 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-компонента определите 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);
}

Надеюсь это поможет!

29
29.03.2014 12:44:35

Я сам задал тот же вопрос здесь и только что опубликовал ответ, который недавно нашел. Краткий ответ: введите SecurityContext, и обратитесь SecurityContextHolderтолько в вашей конфигурации Spring, чтобы получитьSecurityContext

4
23.05.2017 11:47:16

Использование статики в этом случае - лучший способ написать безопасный код.

Да, статика вообще плохая - как правило, но в этом случае статика - это то, что вам нужно. Поскольку контекст безопасности связывает принципала с текущим работающим потоком, наиболее безопасный код будет обращаться к статическому потоку из потока как можно напрямую. Скрытие доступа за внедренным классом-оболочкой дает злоумышленнику больше очков для атаки. Им не понадобится доступ к коду (который им будет сложно изменить, если jar был подписан), им просто нужен способ переопределить конфигурацию, что можно сделать во время выполнения или вставить какой-то XML в путь к классам. Даже использование внедрения аннотаций может быть заменено внешним XML. Такой XML может внедрить в работающую систему мошеннического принципала.

7
4.02.2010 12:59:51

Лично я бы просто использовал 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, однако он очень гибкий и позволяет вам проводить модульное тестирование для сценариев, таких как нулевые объекты аутентификации и т.д. без необходимости изменения вашего (не тестового) кода

9
7.03.2012 21:03:26

Просто сделайте это обычным способом, а затем вставьте его, используя 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);
159
1.12.2015 06:15:26
@ Леонардо, где это должно Authentication aбыть добавлено в контроллере? Как я могу понять в каждом вызове метода? Это нормально для «весеннего пути» просто добавить его, а не вводить?
Oleg Kuts 25.04.2016 20:14:48
Но помните, что он не будет работать с TestNG, потому что SecurityContextHolder хранит локальную переменную потока, поэтому вы разделяете эту переменную между тестами ...
Łukasz Woźniczka 14.03.2017 09:46:17
Сделайте это в @BeforeEach(JUnit5) или @Before(JUnit 4). Хорошо и просто.
WesternGun 26.11.2019 11:37:53

Аутентификация - это свойство потока в серверной среде так же, как это свойство процесса в ОС. Наличие экземпляра бина для доступа к информации аутентификации было бы неудобной конфигурацией и накладными расходами на подключение без какой-либо выгоды.

Что касается тестовой аутентификации, есть несколько способов облегчить вашу жизнь. Мое любимое - сделать пользовательскую аннотацию @Authenticatedи прослушиватель выполнения теста, который управляет им. Проверьте DirtiesContextTestExecutionListenerна вдохновение.

1
20.06.2013 20:55:16

После довольно большой работы мне удалось воспроизвести желаемое поведение. Я эмулировал логин через 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 
}
0
11.06.2014 10:59:08

генеральный

Тем временем (начиная с версии 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;
        }
    };
}
2
16.11.2015 12:07:02

Не отвечая на вопрос о том, как создавать и внедрять объекты аутентификации, 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 (из которых были скопированы приведенные выше примеры)

28
4.05.2019 12:24:05