Spring Тест и безопасность: как имитировать аутентификацию?
Я пытался выяснить, как unit test, если мои URL-адреса моих контроллеров должным образом защищены. На всякий случай кто-то меняет ситуацию и случайно удаляет настройки безопасности.
Мой метод контроллера выглядит следующим образом:
@RequestMapping("/api/v1/resource/test")
@Secured("ROLE_USER")
public @ResonseBody String test() {
return "test";
}
Я создал WebTestEnvironment следующим образом:
import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({
"file:src/main/webapp/WEB-INF/spring/security.xml",
"file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
"file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {
@Resource
private FilterChainProxy springSecurityFilterChain;
@Autowired
@Qualifier("databaseUserService")
protected UserDetailsService userDetailsService;
@Autowired
private WebApplicationContext wac;
@Autowired
protected DataSource dataSource;
protected MockMvc mockMvc;
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected UsernamePasswordAuthenticationToken getPrincipal(String username) {
UserDetails user = this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities());
return authentication;
}
@Before
public void setupMockMvc() throws NamingException {
// setup mock MVC
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.wac)
.addFilters(this.springSecurityFilterChain)
.build();
}
}
В моем фактическом тесте я попытался сделать что-то вроде этого:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class CopyOfClaimTest extends WebappTestEnvironment {
@Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
SecurityContextHolder.getContext().setAuthentication(principal);
super.mockMvc
.perform(
get("/api/v1/resource/test")
// .principal(principal)
.session(session))
.andExpect(status().isOk());
}
}
Я выбрал это здесь:
Однако, если вы посмотрите внимательно, это помогает только при отправке фактических запросов на URL-адреса, но только при тестировании служб на уровне функций. В моем случае было исключено исключение "access denied":
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
...
Следует отметить следующие два сообщения журнала, в основном говорящие, что ни один пользователь не был аутентифицирован, указав, что установка Principal
не работает или что она была перезаписана.
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.sprin[email protected]9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.sprin[email protected]957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
Ответы
Ответ 1
Оказалось, что SecurityContextPersistenceFilter
, являющийся частью цепи фильтра безопасности Spring, всегда сбрасывает мой SecurityContext
, который я установил для вызова SecurityContextHolder.getContext().setAuthentication(principal)
(или с помощью метода .principal(principal)
). Этот фильтр устанавливает SecurityContext
в SecurityContextHolder
с SecurityContext
из SecurityContextRepository
НАВЕРШЕНИЯ того, который я установил ранее. По умолчанию репозиторий является HttpSessionSecurityContextRepository
. HttpSessionSecurityContextRepository
проверяет данный HttpRequest
и пытается получить доступ к соответствующему HttpSession
. Если он существует, он попытается прочитать SecurityContext
из HttpSession
. Если это не удается, репозиторий генерирует пустой SecurityContext
.
Таким образом, мое решение состоит в том, чтобы передать HttpSession
вместе с запросом, который содержит SecurityContext
:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class Test extends WebappTestEnvironment {
public static class MockSecurityContext implements SecurityContext {
private static final long serialVersionUID = -1386535243513362694L;
private Authentication authentication;
public MockSecurityContext(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
@Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
new MockSecurityContext(principal));
super.mockMvc
.perform(
get("/api/v1/resource/test")
.session(session))
.andExpect(status().isOk());
}
}
Ответ 2
Добавить в pom.xml:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.RC2</version>
</dependency>
и используйте org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
для запроса авторизации.
См. Пример использования https://github.com/rwinch/spring-security-test-blog
(https://jira.spring.io/browse/SEC-2592).
Update:
4.0.0.RC2 работает для spring -security 3.x.
Для spring -security 4 spring -security-test становится частью spring -security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, версия такая же).
Настройка изменена: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
Пример базовой аутентификации: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.
Ответ 3
Упорядочение для ответа Я не мог найти ни одного, чтобы быть простым и гибким одновременно, тогда я нашел Spring Ссылка на безопасность и я понял, что есть почти идеальные решения. Решения AOP часто являются самыми большими для тестирования, а Spring предоставляет ему @WithMockUser
, @WithUserDetails
и @WithSecurityContext
в этом артефакте:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
В большинстве случаев @WithUserDetails
собирает гибкость и мощь, которые мне нужны.
Как работает @WithUserDetails?
В основном вам просто нужно создать пользовательский UserDetailsService
со всеми возможными профилями пользователей, которые вы хотите протестировать. Например
@TestConfiguration
public class SpringSecurityWebAuxTestConfig {
@Bean
@Primary
public UserDetailsService userDetailsService() {
User basicUser = new UserImpl("Basic User", "[email protected]", "password");
UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("PERM_FOO_READ")
));
User managerUser = new UserImpl("Manager User", "[email protected]", "password");
UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_MANAGER"),
new SimpleGrantedAuthority("PERM_FOO_READ"),
new SimpleGrantedAuthority("PERM_FOO_WRITE"),
new SimpleGrantedAuthority("PERM_FOO_MANAGE")
));
return new InMemoryUserDetailsManager(Arrays.asList(
basicActiveUser, managerActiveUser
));
}
}
Теперь у нас есть наши пользователи, поэтому представьте, что мы хотим протестировать управление доступом к этой функции контроллера:
@RestController
@RequestMapping("/foo")
public class FooController {
@Secured("ROLE_MANAGER")
@GetMapping("/salute")
public String saluteYourManager(@AuthenticationPrincipal User activeUser)
{
return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
}
}
Здесь у нас есть отображаемая функция маршрута /foo/салют, и мы тестируем безопасность на основе ролей с аннотацией @Secured
, хотя вы можете протестировать @PreAuthorize
и @PostAuthorize
также.
Давайте создадим два теста: один, чтобы проверить, может ли действительный пользователь видеть этот ответ салюта, а другой проверить, действительно ли он запрещен.
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails("[email protected]")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("[email protected]")));
}
@Test
@WithUserDetails("[email protected]")
public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isForbidden());
}
}
Как вы видите, мы импортировали SpringSecurityWebAuxTestConfig
, чтобы предоставить нашим пользователям тестирование. Каждый из них использовался в соответствующем тестовом примере, просто используя простое аннотирование, уменьшая код и сложность.
Лучше использовать @WithMockUser для более простой защиты на основе ролей
Как вы видите, @WithUserDetails
обладает всей гибкостью, необходимой для большинства ваших приложений. Он позволяет использовать пользовательских пользователей с любыми GrantedAuthority, такими как роли или разрешения. Но если вы просто работаете с ролями, тестирование может быть еще проще, и вы можете избежать создания пользовательского UserDetailsService
. В таких случаях укажите простую комбинацию пользователя, пароля и ролей с помощью @WithMockUser.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
String value() default "user";
String username() default "";
String[] roles() default {"USER"};
String password() default "password";
}
Аннотации определяют значения по умолчанию для очень простого пользователя. Как и в нашем случае маршрут, который мы тестируем, требует, чтобы аутентифицированный пользователь был менеджером, мы можем отказаться от использования SpringSecurityWebAuxTestConfig
и сделать это.
@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("user")));
}
Обратите внимание, что теперь вместо пользователя [email protected] мы получаем по умолчанию @WithMockUser
: пользователь; но это не имеет значения, потому что мы действительно заботимся о его роли: ROLE_MANAGER
.
Заключение
Как вы видите с аннотациями типа @WithUserDetails
и @WithMockUser
, мы можем переключаться между различными сценариями аутентифицированных пользователей, не создавая классы, отчужденные от нашей архитектуры, просто для проведения простых тестов. Он также рекомендовал вам увидеть, как @WithSecurityContext работает для еще большей гибкости.
Ответ 4
Вот пример для тех, кто хочет протестировать Spring MockMvc Security Config, используя базовую аутентификацию Base64.
String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
Зависимость Maven
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
Ответ 5
Опции, чтобы избежать использования SecurityContextHolder в тестах:
- Вариант 1: использовать mocks - я имею в виду mock
SecurityContextHolder
с помощью некоторой макетной библиотеки - например, EasyMock
- Вариант 2: wrap call
SecurityContextHolder.get...
в вашем коде в некоторой службе - например, в SecurityServiceImpl
с методом getCurrentPrincipal
, который реализует интерфейс SecurityService
, а затем в ваших тестах вы можете просто создать макетную реализацию этого интерфейса, которая возвращает желаемого принципала без доступа к SecurityContextHolder
.
Ответ 6
Короткий ответ:
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private Filter springSecurityFilterChain;
@Before
public void setUp() throws Exception {
final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.defaultRequest(defaultRequestBuilder)
.alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
.apply(springSecurity(springSecurityFilterChain))
.build();
}
private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
final MockHttpServletRequest request) {
requestBuilder.session((MockHttpSession) request.getSession());
return request;
}
После выполнения formLogin
из spring теста безопасности каждый из ваших запросов будет автоматически вызываться как зарегистрированный пользователь.
Длинный ответ:
Проверьте это решение (ответ на spring 4): Как войти в систему с помощью spring 3.2 нового теста mvc
Ответ 7
Так как Spring 4.0+, лучшим решением является аннотация метода тестирования с помощью @WithMockUser
@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
mockMvc.perform(get("/someApi"))
.andExpect(status().isOk());
}
Не забудьте добавить следующую зависимость к вашему проекту
'org.springframework.security:spring-security-test:4.2.3.RELEASE'