Модульное тестирование с Spring Security - PullRequest
123 голосов
/ 11 декабря 2008

Моя компания оценивает 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 ]

135 голосов
/ 20 июня 2013

Просто сделайте это обычным способом, а затем вставьте его, используя 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);
41 голосов
/ 16 декабря 2008

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

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

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

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

28 голосов
/ 28 декабря 2008

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

cliff.meyers упомянул об одном способе - создать свой собственный «основной» тип и внедрить экземпляр в потребителей. Тег Spring <<a href="http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-factory-scopes-other-injection" rel="noreferrer"> 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);
}

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

21 голосов
/ 25 мая 2016

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

8 голосов
/ 08 марта 2012

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

5 голосов
/ 04 февраля 2010

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

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

3 голосов
/ 20 мая 2009

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

2 голосов
/ 16 ноября 2015

Общее

Тем временем (начиная с версии 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 голосов
/ 11 декабря 2008

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

1 голос
/ 20 июня 2013

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...