Spring Test& Security: jak sfałszować uwierzytelnianie?
Próbowałem dowiedzieć się, jak przetestować jednostki, czy Moje adresy URL moich kontrolerów są prawidłowo zabezpieczone. Na wypadek, gdyby ktoś coś zmienił i przypadkowo usunął ustawienia zabezpieczeń.
Moja metoda kontrolera wygląda tak:
@RequestMapping("/api/v1/resource/test")
@Secured("ROLE_USER")
public @ResonseBody String test() {
return "test";
}
Założyłem taki 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();
}
}
W moim prawdziwym teście próbowałem zrobić coś takiego:
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());
}
}
Podniosłem to tutaj:
- http://java.dzone.com/articles/spring-test-mvc-junit-testing tutaj:
- http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller{[19]lub tutaj:
- Jak JUnit testuje adnotację @ Preautoryzuj i jej sprężynę el określoną przez kontroler spring MVC?
Jednak jeśli się przyjrzeć, to pomaga tylko wtedy, gdy nie wysyła rzeczywistych wniosków do Adresów URL, ale tylko podczas testowania usług na poziomie funkcji. W moim przypadku został wyrzucony wyjątek "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]
...
Na uwagę zasługują następujące dwa komunikaty dziennika, które mówią, że żaden użytkownik nie został uwierzytelniony, wskazując, że ustawienie Principal
nie zadziałało lub zostało nadpisane.
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 answers
Okazało się, że SecurityContextPersistenceFilter
, który jest częścią łańcucha filtrów zabezpieczających sprężynę, zawsze resetuje moje SecurityContext
, które ustawiłem wywołaniem SecurityContextHolder.getContext().setAuthentication(principal)
(lub przy użyciu metody .principal(principal)
). Ten filtr Ustawia SecurityContext
w SecurityContextHolder
z SecurityContext
z SecurityContextRepository
nadpisanie tego, który ustawiłem wcześniej. Repozytorium to domyślnie HttpSessionSecurityContextRepository
. HttpSessionSecurityContextRepository
sprawdza podany HttpRequest
i próbuje uzyskać dostęp do odpowiedniego HttpSession
. Jeśli istnieje, spróbuje odczytać {[2] } z HttpSession
. Jeśli to się nie powiedzie, repozytorium generuje puste SecurityContext
.
HttpSession
wraz z żądaniem, które zawiera 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());
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-09 22:58:05
Seaching dla odpowiedzi nie mogłem znaleźć żadnego, aby być łatwym i elastycznym w tym samym czasie, potem znalazłem Spring Security Reference i zdałem sobie sprawę, że są blisko idealnych rozwiązań. Rozwiązania AOP często są najlepsze do testowania, A Spring zapewnia mu @WithMockUser
, @WithUserDetails
i @WithSecurityContext
, w tym artefakcie:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
W większości przypadków, @WithUserDetails
gromadzi elastyczność i moc, której potrzebuję.
Jak działa @ WithUserDetails?
W zasadzie wystarczy stworzyć custom UserDetailsService
ze wszystkimi możliwymi profilami użytkowników, które chcesz przetestować. E. g
@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
));
}
}
Teraz mamy naszych użytkowników gotowych, więc wyobraź sobie, że chcemy przetestować kontrolę dostępu do tej funkcji kontrolera: {]}
@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());
}
}
Tutaj mamy get mapped function do trasy /foo/salute i testujemy zabezpieczenia oparte na rolach z adnotacją @Secured
, chociaż możesz również przetestować @PreAuthorize
i @PostAuthorize
.
Stwórzmy dwa testy, jeden, aby sprawdzić, czy prawidłowy użytkownik może to zobaczyć odpowiedź i drugi, aby sprawdzić, czy jest to rzeczywiście zabronione.
@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());
}
}
Jak widzisz zaimportowaliśmy SpringSecurityWebAuxTestConfig
, aby zapewnić naszym użytkownikom testy. Każdy z nich został użyty w odpowiednim przypadku testowym poprzez użycie prostej adnotacji, zmniejszając kod i złożoność.
Lepiej używać @ WithMockUser dla prostszego bezpieczeństwa opartego na rolach
Jak widzisz @WithUserDetails
ma całą elastyczność, jakiej potrzebujesz dla większości aplikacji. Pozwala na korzystanie z niestandardowych użytkowników z dowolnym GrantedAuthority, takich jak role lub uprawnienia. Ale jeśli pracujesz tylko z rolami, testowanie może być jeszcze łatwiejsze i możesz uniknąć konstruowania niestandardowego UserDetailsService
. W takich przypadkach określ prostą kombinację użytkownika, hasła i ról za pomocą @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";
}
Adnotacja definiuje domyślne wartości dla bardzo podstawowego użytkownika. Ponieważ w naszym przypadku testowana trasa wymaga tylko, aby uwierzytelniony użytkownik był menedżerem, możemy zakończyć korzystanie z SpringSecurityWebAuxTestConfig
i zrobić to.
@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")));
}
Zauważ, że teraz zamiast użytkownika [email protected] otrzymujemy domyślną wartość dostarczoną przez @WithMockUser
: user ; ale to nie ma znaczenia, bo tak naprawdę zależy nam na jego roli: ROLE_MANAGER
.
Wnioski
Jak widzisz z adnotacjami, takimi jak @WithUserDetails
i @WithMockUser
, możemy przełączać się między różnymi scenariuszami uwierzytelnionych użytkowników bez budowania klas wyobcowanych z naszej architektury tylko dla wykonywania prostych testów. Zaleca się również, aby zobaczyć, jak @ WithSecurityContext Działa na jeszcze większą elastyczność.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-01 17:10:21
Dodaj pom.xml:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.RC2</version>
</dependency>
I użyj org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
do żądania autoryzacji.
Zobacz przykładowe użycie w https://github.com/rwinch/spring-security-test-blog
( https://jira.spring.io/browse/SEC-2592).
Aktualizacja:
4.0.0.RC2 działa na spring-security 3.x. Dla spring-security 4 Spring-security-test zostań częścią spring-security ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , wersja jest taka sama).
Konfiguracja została zmieniona: 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();
}
Próbka dla uwierzytelniania podstawowego: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 14:11:40
Od wiosny 4.0+ najlepszym rozwiązaniem jest adnotacja metody testowej za pomocą @ WithMockUser
@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
mockMvc.perform(get("/someApi"))
.andExpect(status().isOk());
}
Pamiętaj, aby dodać następującą zależność do projektu
'org.springframework.security:spring-security-test:4.2.3.RELEASE'
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-30 13:51:27
Oto przykład dla tych, którzy chcą przetestować konfigurację zabezpieczeń Spring MockMvc przy użyciu uwierzytelniania Base64 basic.
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());
Zależność Mavena
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-25 18:33:14
Krótka odpowiedź:
@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;
}
Po wykonaniu formLogin
Z spring security test każde z Twoich zapytań zostanie automatycznie wywołane jako zalogowany użytkownik.
Długa odpowiedź:
Sprawdź To rozwiązanie (odpowiedź jest dla spring 4): Jak zalogować się do użytkownika z spring 3.2 new mvc testing
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-02 07:49:31
Opcje unikania SecurityContextHolder w testach:
-
Option 1 : use mocks-mam na myśli mock
SecurityContextHolder
using some mock library - EasyMock for example -
Opcja 2 : zawijanie wywołania
SecurityContextHolder.get...
w kodzie w jakiejś usłudze - na przykład w {[2] } z metodągetCurrentPrincipal
implementującą interfejsSecurityService
, a następnie w testach możesz po prostu stworzyć przykładową implementację tego interfejsu, która zwróci żądaną wartość główną bez dostępu doSecurityContextHolder
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-04 17:38:25