2008-12-11 4 views
94

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

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

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

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

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

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

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

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

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

Это кажется чересчур подробным. Есть ли более простой способ?

Сам SecurityContextHolder объект кажется очень не-весеннему ...

ответ

31

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

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

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

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

+0

Спасибо, это полезный совет. То, что я сделал до сих пор, в основном заключается в продолжении вызова SecurityContextHolder.getContext() (через несколько собственных методов-оболочек, поэтому, по крайней мере, он вызван только из одного класса). – 2008-12-16 19:56:39

+2

Хотя одна заметка - я не думаю, что у ServletContextHolder есть какая-либо концепция HttpSession или способ узнать, работает ли она в среде веб-сервера - она ​​использует ThreadLocal, если вы не настроите ее на использование чего-то другого (только два других встроенных режима: InheritableThreadLocal и Global) – 2008-12-16 19:57:19

+0

Единственный недостаток использования бобов, зависящих от сеанса/запроса, весной заключается в том, что они потерпят неудачу в тесте JUnit. Что вы можете сделать, так это реализовать настраиваемую область, которая будет использовать сеанс/запрос, если она доступна, и вернуться к потоку. Я предполагаю, что Spring Security делает что-то подобное ... – 2008-12-16 21:30:07

2

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

+0

Хотя эти тестовые классы полезны, я не уверен, если они применяются здесь.Мои тесты не имеют понятия ApplicationContext - они им не нужны. Все, что мне нужно, это убедиться, что SecurityContext заполнен до запуска тестового метода - он просто чувствует себя грязным, чтобы сначала установить его в ThreadLocal – 2008-12-11 19:31:59

26

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

Благодаря магии АОПА: область видимости прокси тег, статический метод getUserDetails будет вызываться каждый раз, когда новый запрос HTTP и любые ссылки на свойство currentUser будут правильно решены. Теперь модульное тестирование становится тривиальным:

protected void setUp() { 
    // existing init code 

    MyUserDetails user = new MyUserDetails(); 
    // set up user as you wish 
    controller.setCurrentUser(user); 
} 

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

3

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

5

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

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

8

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

97

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

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

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

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

Общие

В то же время (начиная с версии 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; 
     } 
    }; 
} 
16

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

Смежные вопросы