Тестування блоку за допомогою Spring Security


140

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

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

У створеному нами прототипі я зберігав об’єкт "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()));
    ...
}

Це здається надмірно багатослівним. Чи є простіший спосіб?

Сам SecurityContextHolderоб’єкт здається дуже не-весняним ...

Відповіді:


48

Проблема полягає в тому, що Spring Security не робить об’єкт аутентифікації доступним як боб у контейнері, тому немає можливості легко ввести його або автоматично провести його з коробки.

Перш ніж ми почали використовувати Spring Security, ми створили б бобовий сеанс в контейнері для зберігання Principal, вводимо його в "AuthenticationService" (однократний), а потім введемо цей квасоля в інші сервіси, які потребували знання поточного Principal.

Якщо ви реалізуєте власну службу аутентифікації, ви можете в основному зробити те ж саме: створити об'єм сеансу з основним властивістю, ввести це в службу аутентифікації, сервіс аутентифікації встановити властивість на успішному авт., А потім зробіть послугу auth доступною для інших бобів, скільки вам потрібно.

Я б не почував себе занадто погано щодо використання SecurityContextHolder. хоч. Я знаю, що це статичний / Singleton і що Spring відлякує від використання таких речей, але їх реалізація вимагає поводитись належним чином залежно від середовища: сеанс, який проходить у контейнері Servlet, обсяг потоку в тесті JUnit тощо. Справжній обмежуючий фактор Singleton - це тоді, коли він забезпечує реалізацію, негнучку до різних середовищ.


Дякую, це корисна порада. Що я до цього часу робив - це в основному перейти до виклику SecurityContextHolder.getContext () (через кілька власних методів обгортки, тому принаймні його називають лише з одного класу).
matt b

2
Хоча лише одна примітка - я не думаю, що ServletContextHolder не має жодної концепції HttpSession або способу дізнатися, чи працює він у середовищі веб-сервера - він використовує ThreadLocal, якщо ви не налаштовуєте його на використання іншого (єдині два вбудовані режими - InheritableThreadLocal і Global)
мат b

Єдиним недоліком використання бобів, пов’язаних із сеансом / запитом навесні, є те, що вони не зможуть пройти тест JUnit. Що ви можете зробити, це реалізувати власну область, яка використовуватиме сеанс / запит, якщо вони доступні, і повернутися до потоку. Я здогадуюсь, що Spring Security робить щось подібне ...
cliff.meyers

Моя мета - побудувати відпочинковий api без сесій. Можливо, з освіжаючим знаком. Поки це не відповіло на моє запитання, це допомогло. Спасибі
Pomagranite

166

Просто зробіть це звичайним способом, а потім вставте його, використовуючи 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);

2
@ Леонардо, де це Authentication aслід додати в контролері? Як я можу зрозуміти виклик кожного методу? Чи добре для "весняного способу" просто додати його, а не вводити?
Олег Куц

Але пам’ятайте, що він не буде працювати з TestNG, тому що SecurityContextHolder утримує локальну змінну потоку, тож ви поділите цю змінну між тестами ...
Łukasz Woźniczka

Зробіть це в @BeforeEach(JUnit5) або @Before(JUnit 4). Добре і просто.
WesternGun

30

Не відповідаючи на питання про те, як створити та вставити об'єкти аутентифікації, 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 (з яких скопійовано вищевказані приклади)


29

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

cliff.meyers згадав один із способів його створення - створити власний "основний" тип та ввести інстанцію споживачам. Тег Spring < aop: scoped-proxy />, що вводиться в 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 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);
}

Сподіваюся, це допомагає!


9

Особисто я просто використовую 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, однак він є дуже гнучким і дозволяє вам проводити тестування сценаріїв, таких як нульові об'єкти аутентифікації тощо, не змінюючи код (не тест)


7

Використання статики в цьому випадку - найкращий спосіб написати захищений код.

Так, статика взагалі погана - загалом, але в цьому випадку статика - це те, що ти хочеш. Оскільки контекст безпеки асоціює Principal з поточним потоком, що виконується, найбезпечніший код отримає доступ до статики з потоку якомога безпосередньо. Приховування доступу за класом обгортки, який вводиться, надає зловмиснику більше очок для атаки. Їм не знадобиться доступ до коду (який би важко змінився, якщо jar був підписаний), їм просто потрібен спосіб переосмислити конфігурацію, що можна зробити під час виконання або просунути якийсь XML на класний шлях. Навіть використання ін’єкцій із примітками було б надмірно зручним із зовнішнім XML. Такий XML може вводити працюючу систему із шахрайським принципом.


4

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


3

Загальні

Тим часом (починаючи з версії 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

Я хотів би поглянути на абстрактні тестові класи Spring та знущатися над предметами, про які тут йдеться . Вони забезпечують потужний спосіб автоматичного підключення об'єктів, якими керують Spring, полегшуючи пристрій та інтеграцію тестування.


Хоча ці тестові заняття корисні, я не впевнений, чи застосовуються вони тут. Мої тести не мають поняття ApplicationContext - вони не потрібні. Все, що мені потрібно, це переконатися, що SecurityContext заповнений перед запуском тестового методу - він просто забруднений, щоб його потрібно було встановити в ThreadLocal спочатку
matt b

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 
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.