Как сделать unit test spring безопасность @PreAuthorize (hasRole)?

Что мне нужно для unit test части hasRole аннотации PreAuthorize по методу контроллера?

Мой тест должен быть успешным, потому что у зарегистрированного пользователя есть только одна из двух ролей, но вместо этого он терпит неудачу со следующей ошибкой утверждения:

java.lang.AssertionError: Status

Ожидаемое: 401

Актуально: 200

У меня есть следующий метод в MyController:

@PreAuthorize(value = "hasRole('MY_ROLE') and hasRole('MY_SECOND_ROLE')")
@RequestMapping(value = "/myurl", method = RequestMethod.GET)
public String loadPage(Model model, Authentication authentication, HttpSession session) {
    ...stuff to do...
}

Я создал следующий abstract-security-test.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">

    <security:global-method-security secured-annotations="enabled" />

    <security:authentication-manager alias="authManager">
        <security:authentication-provider>
            <security:user-service>
                <security:user name="missingsecondrole" password="user" authorities="MY_ROLE" />
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>

</beans>

И в моем unit test у меня есть это:

@ContextConfiguration("classpath:/spring/abstract-security-test.xml")
public class MyTest {
    private final MyController myController = new MyController();
    @Autowired
    private AuthenticationManager manager;

    @Test
    public void testValidUserWithInvalidRoleFails() throws Exception {
        MockMvc mockMvc = standaloneSetup(myController).setViewResolvers(viewResolver()).build();

        Authentication auth = login("missingsecondrole", "user");

        mockMvc.perform(get("/myurl")
            .session(session)
            .flashAttr(MODEL_ATTRIBUTE_NAME, new ModelMap())
            .principal(auth)).andExpect(status().isUnauthorized());
    }

    protected Authentication login(String name, String password) {
        Authentication auth = new UsernamePasswordAuthenticationToken(name, password);
        SecurityContextHolder.getContext().setAuthentication(manager.authenticate(auth));
        return auth;
    }

    private ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("WEB-INF/views");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

Ответы

Ответ 1

UPDATE

Spring Безопасность 4 обеспечивает всестороннюю поддержку для интеграции с MockMvc. Например:

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class SecurityMockMvcTests {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

    @Test
    public void withUserRequestPostProcessor() {
        mvc
            .perform(get("/admin").with(user("admin").roles("USER","ADMIN")))
            ...
    }

    @WithMockUser(roles="ADMIN")
    @Test
    public void withMockUser() {
        mvc
            .perform(get("/admin"))
            ...
    }

 ...

Проблема

Проблема заключается в том, что установка SecurityContextHolder не работает в этом случае. Причина в том, что SecurityContextPersistenceFilter будет использовать SecurityContextRepository, чтобы попытаться выяснить SecurityContext из HttpServletRequest (по умолчанию он использует HttpSession). SecurityContext, который находит (или не находит), переопределит SecurityContext, который вы установили в SecurityContextHolder.

Решение

Чтобы убедиться, что запрос аутентифицирован, вам необходимо связать свой SecurityContext с помощью SecurityContextRepository, который вы используете. По умолчанию используется HttpSessionSecurityContextRepository. Примерный метод, который позволит вам высмеивать вход пользователя пользователем:

private SecurityContextRepository repository = 
      new HttpSessionSecurityContextRepository();

private void login(SecurityContext securityContext, HttpServletRequest request) {
    HttpServletResponse response = new MockHttpServletResponse();

    HttpRequestResponseHolder requestResponseHolder = 
          new HttpRequestResponseHolder(request, response);
    repository.loadContext(requestResponseHolder);

    request = requestResponseHolder.getRequest();
    response = requestResponseHolder.getResponse();

    repository.saveContext(securityContext, request, response);
}

Детали использования этого могут быть немного неопределенными, поскольку вы можете не знать, как получить доступ к HttpServletRequest в MockMvc, но продолжайте читать, поскольку есть лучшее решение.

Упрощение

Если вы хотите сделать это и другие связанные с безопасностью взаимодействия с MockMvc проще, вы можете обратиться к примерному приложению gs- spring -security-3.2. В рамках проекта вы найдете некоторые утилиты для работы с Spring Security и MockMvc под названием SecurityRequestPostProcessors. Чтобы использовать их, вы можете скопировать этот ранее упомянутый класс в свой проект. Использование этой утилиты позволит вам написать что-то вроде этого:

RequestBuilder request = get("/110")
    .with(user(rob).roles("USER"));

mvc
    .perform(request)
    .andExpect(status().isUnAuthorized());

ПРИМЕЧАНИЕ. Нет необходимости устанавливать принципала в запросе как Spring Безопасность устанавливает для вас Принципала, если пользователь аутентифицирован.

Вы можете найти дополнительные примеры в SecurityTests. Этот проект также поможет в других интеграциях между MockMvc и Spring Security (т.е. Настройке запроса с помощью токена CSRF при выполнении POST).

Не включено по умолчанию?

Вы можете спросить, почему это не включено по умолчанию. Ответ заключается в том, что у нас просто не было времени на 3.2-временную шкалу. Весь код в образце будет работать нормально, но мы не были достаточно уверены в соглашениях об именах и точно как он интегрировался для его выпуска. Вы можете отслеживать SEC-2015, который планируется выпустить с помощью Spring Security 4.0.0.M1.

Обновление

В вашем экземпляре MockMvc также должен быть файл springSecurityFilterChain. Для этого вы можете использовать следующее:

@Autowired
private Filter springSecurityFilterChain;

@Test
public void testValidUserWithInvalidRoleFails() throws Exception {
    MockMvc mockMvc = standaloneSetup(myController)
        .addFilters(springSecurityFilterChain)
        .setViewResolvers(viewResolver())
        .build();
    ...

Чтобы работать @Autowired, вам необходимо включить свою конфигурацию безопасности, которая делает springSecurityFilterChain в вашем @ContextConfiguration. Для вашей текущей настройки это означает, что "classpath:/spring/abstract-security-test.xml" должен содержать вашу часть <http ..> вашей конфигурации безопасности (и все зависимые beans). Кроме того, вы можете включить второй файл в @ContextConfiguration, у которого есть ваша <http ..> часть вашей конфигурации безопасности (и все зависимые beans).

Ответ 2

Чтобы добавить решение Rob выше, по состоянию на 20 декабря 2014 года в классе SecurityRequestPostProcessors на главной ветке есть ответ от ответа Rob, который запрещает заполнять назначенные роли.

Быстрое исправление заключается в том, чтобы прокомментировать следующую строку кода (в настоящее время строка 181) в методе roles(String... roles) внутреннего статического класса UserRequestPostProcessor SecurityRequestPostProcessors:

// List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);.

Вам нужно прокомментировать локальную переменную, а не переменную-член.

В качестве альтернативы вы можете вставить эту строку непосредственно перед возвратом из метода:

this.authorities = authorities;

P.S Я бы добавил это в качестве комментария, если бы у меня была достаточно репутации.