Испытание блока с безопасностью весны



моя компания оценивает 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 сам объект, кажется, очень не по-весеннему...

592   11  

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 эмулировать a UserDetails вернулся из 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() чтобы ввести пользователя следующим образом:

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;
        }
    };
}

аутентификация-это свойство потока в серверной среде так же, как и свойство процесса в ОС. Наличие экземпляра 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

    Ничего не найдено.