Тестирование устройств с помощью Spring Безопасность

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

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

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

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

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

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

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

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

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

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

Сам объект SecurityContextHolder кажется очень не-w766 > -like...

Ответ 1

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

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

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

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

Ответ 2

Просто делайте это обычным способом, а затем вставьте его с помощью 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);

Ответ 3

Вы совершенно правы, что беспокоитесь - вызовы статических методов особенно проблематичны для модульного тестирования, поскольку вы не можете легко смоделировать свои зависимости. Я собираюсь показать вам, как позволить контейнеру 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);
}

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

Ответ 4

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

Ответ 5

Лично я бы просто использовал Powermock вместе с Mockito или Easymock, чтобы издеваться над статическим SecurityContextHolder.getSecurityContext() в вашем тесте unit/integration, например.

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

Ответ 9

Общие

Тем временем (начиная с версии 3.2, в 2013 году, благодаря SEC-2298), аутентификация может быть введена в MVC-методы, используя аннотация @AuthenticationPrincipal:

@Controller
class Controller {
  @RequestMapping("/somewhere")
  public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
  }
}

Испытания

В unit test вы можете явно вызвать этот метод напрямую. В тестах интеграции с использованием 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;
        }
    };
}

Ответ 10

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

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

Ответ 11

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