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

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

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

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

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

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

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

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

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

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

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

Сам объект SecurityContextHolder выглядит совсем не как Spring ...

112 голосов | спросил matt b 11 ThuEurope/Moscow2008-12-11T22:11:43+03:00Europe/Moscow12bEurope/MoscowThu, 11 Dec 2008 22:11:43 +0300 2008, 22:11:43

11 ответов


0

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

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

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

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

ответил cliff.meyers 16 TueEurope/Moscow2008-12-16T22:27:37+03:00Europe/Moscow12bEurope/MoscowTue, 16 Dec 2008 22:27:37 +0300 2008, 22:27:37
0

Просто сделайте это обычным способом, а затем вставьте его, используя 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);
ответил Leonardo Eloy 20 J0000006Europe/Moscow 2013, 22:17:21
0

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

cliff.meyers упомянул об одном способе - создать свой собственный «основной» тип и внедрить экземпляр в потребителей. Источник АОП: область видимости прокси /> тег, введенный в версии 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);
}

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

ответил Leonardo Eloy 20 J0000006Europe/Moscow 2013, 22:17:21
0

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

ответил matsev 25 Maypm16 2016, 22:13:26
0

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

ответил 8 MaramThu, 08 Mar 2012 01:03:26 +04002012-03-08T01:03:26+04:0001 2012, 01:03:26
0

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

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

ответил Michael Bushe 4 FebruaryEurope/MoscowbThu, 04 Feb 2010 15:59:51 +0300000000pmThu, 04 Feb 2010 15:59:51 +030010 2010, 15:59:51
0

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

ответил Scott Bale 20 Mayam09 2009, 01:45:04
0

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

ответил digitalsanctum 11 ThuEurope/Moscow2008-12-11T22:20:09+03:00Europe/Moscow12bEurope/MoscowThu, 11 Dec 2008 22:20:09 +0300 2008, 22:20:09
0

Общие

Тем временем (начиная с версии 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;
        }
    };
}
ответил yankee 16 12015vEurope/Moscow11bEurope/MoscowMon, 16 Nov 2015 14:52:58 +0300 2015, 14:52:58
0

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

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

ответил Pavel Horal 20 J0000006Europe/Moscow 2013, 22:39:31
0

После довольно большой работы я смог воспроизвести желаемое поведение. Я эмулировал логин через 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 
}
ответил borjab 11 J0000006Europe/Moscow 2014, 14:59:08

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132