Весенний тест и безопасность: как имитировать аутентификацию?
Я пытался выяснить, как модульный тест, если мои 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());
}
}
Я взял это здесь:
- http://java.dzone.com/articles/spring-test-mvc-junit-testing Вот:
- http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller или вот:
- как в JUnit тесты @авторизацию аннотации и весны Эль указанной пружиной контроллер MVC?
но если это помогает только при отправке фактических запросов на URL-адреса, но только при тестировании служб на функциональном уровне. В моем случае было выдано исключение "доступ запрещен":
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.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
7 ответов:
выяснилось, что
SecurityContextPersistenceFilter
, который является частью цепочки фильтров безопасности Spring, всегда сбрасывает mySecurityContext
, который я предлагаю называть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()); } }
Seaching для ответа я не мог найти ни одного, чтобы быть легким и гибким в то же время, то я нашел Spring Security Reference и я понял, что есть почти идеальные решения. Решения 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", "user@company.com", "password"); UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList( new SimpleGrantedAuthority("ROLE_USER"), new SimpleGrantedAuthority("PERM_FOO_READ") )); User managerUser = new UserImpl("Manager User", "manager@company.com", "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 / salute и мы тестируем безопасность на основе ролей с помощью
@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("manager@company.com") public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isOk()) .andExpect(content().string(containsString("manager@company.com"))); } @Test @WithUserDetails("user@company.com") 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"))); }
обратите внимание, что теперь вместо пользователя manager@company.com мы получаем значение по умолчанию, предоставленное
@WithMockUser
:пользователей, но это не важно, потому что то, что мы действительно заботимся о его роли:ROLE_MANAGER
.выводы
как вы видите с аннотациями, как
@WithUserDetails
и@WithMockUser
у нас можете переключатель между различными сценариями аутентифицированных пользователей без создания классов, отчужденных от нашей архитектуры только для проведения простых тестов. Его также рекомендуется посмотреть, как @WithSecurityContext работы для еще большей гибкости.
добавить в 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).обновление:
4.0.0.RC2 работает для весны-безопасности 3.икс. Для весны-обеспеченность 4 весна-обеспеченность-испытание будет частью весны-обеспеченности (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.
начиная с весны 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'
вот пример для тех, кто хочет протестировать 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>
короткий ответ:
@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 security test каждый из ваших запросов будет автоматически вызываться как зарегистрированный пользователь.ответ:
проверьте это решение (ответ для весны 4):как войти в систему пользователя с spring 3.2 new MVC testing
параметры, чтобы избежать использования SecurityContextHolder в тестах:
- 1: используйте насмешки-я имею в виду издеваться
SecurityContextHolder
используя некоторые макет библиотеки -EasyMock- 2: обернуть вызов
SecurityContextHolder.get...
в вашем коде в какой - то Службе-например вSecurityServiceImpl
методомgetCurrentPrincipal
что реализуетSecurityService
интерфейс, а затем в ваших тестах вы можете просто создать макет реализации этого интерфейса, который возвращает желаемый Принципал без доступа кSecurityContextHolder
.