Próbowałem dowiedzieć się, jak przeprowadzić test jednostkowy, czy moje adresy URL moich kontrolerów są odpowiednio zabezpieczone. Na wypadek, gdyby ktoś zmienił rzeczy i przypadkowo usunął ustawienia zabezpieczeń.
Moja metoda kontrolera wygląda następująco:
@RequestMapping("/api/v1/resource/test")
@Secured("ROLE_USER")
public @ResonseBody String test() {
return "test";
}
Skonfigurowałem WebTestEnvironment w następujący sposób:
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 rzeczywistym 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 lub tutaj:
- Jak JUnit testuje adnotację @PreAuthorize i jej sprężynowy EL określony przez sprężynowy kontroler MVC?
Jednak jeśli przyjrzeć się bliżej, pomaga to tylko wtedy, gdy nie wysyłamy rzeczywistych żądań do adresów URL, ale tylko podczas testowania usług na poziomie funkcji. W moim przypadku został zgłoszony wyjątek „odmowa dostępu”:
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]
...
Poniższe dwa komunikaty dziennika są warte uwagi, mówiąc zasadniczo, że żaden użytkownik nie został uwierzytelniony, co wskazuje, ż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
Odpowiedzi:
Szukając odpowiedzi, nie mogłem znaleźć żadnej, która byłaby jednocześnie łatwa i elastyczna, po czym znalazłem Spring Security Reference i zdałem sobie sprawę, że są blisko doskonałych rozwiązań. Rozwiązania AOP często są największymi z nich do testowania, i wiosna zapewnia ją
@WithMockUser
,@WithUserDetails
a@WithSecurityContext
w tym artefaktem:W większości przypadków
@WithUserDetails
zapewnia elastyczność i moc, której potrzebuję.Jak działa @WithUserDetails?
Zasadniczo wystarczy utworzyć niestandardowy
UserDetailsService
ze wszystkimi możliwymi profilami użytkowników, które chcesz przetestować. Na przykładTeraz mamy gotowych naszych użytkowników, więc wyobraź sobie, że chcemy przetestować kontrolę dostępu do tej funkcji kontrolera:
Tutaj mamy funkcję get mapped do route / foo / salute i testujemy zabezpieczenia oparte na rolach z
@Secured
adnotacją, chociaż możesz również przetestować@PreAuthorize
i@PostAuthorize
. Stwórzmy dwa testy, jeden, aby sprawdzić, czy prawidłowy użytkownik może zobaczyć tę odpowiedź pozdrowienia, a drugi, aby sprawdzić, czy jest to rzeczywiście zabronione.Jak widzisz, dokonaliśmy importu,
SpringSecurityWebAuxTestConfig
aby udostępnić naszym użytkownikom do testów. Każdy z nich został użyty w odpowiadającym mu przypadku testowym po prostu za pomocą prostej adnotacji, zmniejszając kod i złożoność.Lepiej użyj @WithMockUser dla prostszego zabezpieczenia opartego na rolach
Jak widzisz,
@WithUserDetails
zapewnia pełną elastyczność, której potrzebujesz do większości aplikacji. Umożliwia korzystanie z niestandardowych użytkowników z dowolnym GrantedAuthority, takim jak role lub uprawnienia. Ale jeśli pracujesz tylko z rolami, testowanie może być jeszcze łatwiejsze i możesz uniknąć tworzenia niestandardowychUserDetailsService
. W takich przypadkach określ prostą kombinację użytkownika, hasła i ról za pomocą @WithMockUser .Adnotacja definiuje wartości domyślne dla bardzo prostego użytkownika. Ponieważ w naszym przypadku testowana przez nas trasa wymaga tylko, aby uwierzytelniony użytkownik był menedżerem, możemy zakończyć korzystanie
SpringSecurityWebAuxTestConfig
i zrobić to.Zauważ, że teraz zamiast user [email protected] otrzymujemy wartość domyślną dostarczoną przez
@WithMockUser
: user ; jeszcze nie będzie to większego znaczenia, ponieważ to, co naprawdę dbają o to jego rolaROLE_MANAGER
.Wnioski
Jak widać z adnotacjami takimi jak
@WithUserDetails
i@WithMockUser
, możemy przełączać się między różnymi scenariuszami uwierzytelnionych użytkowników bez budowania klas wyalienowanych z naszej architektury tylko do wykonywania prostych testów. Zaleca się również, aby zobaczyć, jak działa @WithSecurityContext, zapewniając jeszcze większą elastyczność.źródło
tom
, a drugie przezjerry
?BasicUser
iManager User
podany w odpowiedzi. Kluczową koncepcją jest to, że zamiast dbać o użytkowników, tak naprawdę zależy nam na ich rolach, ale każdy z tych testów, umieszczony w tym samym teście jednostkowym, w rzeczywistości reprezentuje różne zapytania. wykonywane przez różnych użytkowników (z różnymi rolami) do tego samego punktu końcowego.Od wiosny 4.0+ najlepszym rozwiązaniem jest oznaczenie metody testowej adnotacją @WithMockUser
Pamiętaj, aby dodać do projektu następującą zależność
źródło
Okazało się, że
SecurityContextPersistenceFilter
będący częścią łańcucha filtrów Spring Security zawsze resetuje mójSecurityContext
, który ustawiłem wywołującSecurityContextHolder.getContext().setAuthentication(principal)
(lub używając.principal(principal)
metody). Ten filtr ustawiaSecurityContext
in theSecurityContextHolder
z aSecurityContext
from aSecurityContextRepository
OVERWRITING ten, który ustawiłem wcześniej. Repozytorium toHttpSessionSecurityContextRepository
domyślnie. WHttpSessionSecurityContextRepository
kontroluje danyHttpRequest
i próbuje uzyskać dostęp do odpowiadającegoHttpSession
. Jeśli istnieje, spróbuje odczytaćSecurityContext
plikHttpSession
. Jeśli to się nie powiedzie, repozytorium generuje pusty plikSecurityContext
.Zatem moim rozwiązaniem jest przekazanie
HttpSession
wraz z żądaniem, które zawieraSecurityContext
:źródło
getPrincipal()
która moim zdaniem jest nieco myląca. Idealnie powinien zostać nazwanygetAuthentication()
. podobnie w twoimsignedIn()
teście zmienna lokalna powinna być nazwanaauth
lubauthentication
zamiastprincipal
Dodaj w pom.xml:
i użyj
org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
do żądania autoryzacji. Zobacz przykładowe użycie na https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ).Aktualizacja:
4.0.0.RC2 działa dla zabezpieczenia wiosennego 3.x. Do wiosennego testu bezpieczeństwa 4 wiosenny test bezpieczeństwa stał się 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
Przykład podstawowego uwierzytelnienia: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .
źródło
Oto przykład dla tych, którzy chcą przetestować Spring MockMvc Security Config przy użyciu podstawowego uwierzytelniania Base64.
Zależność Mavena
źródło
Krótka odpowiedź:
Po wykonaniu
formLogin
wiosennego testu bezpieczeństwa każde Twoje żądanie będzie automatycznie wywoływane jako zalogowany użytkownik.Długa odpowiedź:
Sprawdź to rozwiązanie (odpowiedź na wiosnę 4): Jak zalogować użytkownika za pomocą nowego testu MVC wiosna 3.2
źródło
Opcje, których należy unikać przy użyciu SecurityContextHolder w testach:
SecurityContextHolder
użyciem jakiejś biblioteki - na przykład EasyMockSecurityContextHolder.get...
w swoim kodzie w jakiejś usłudze - na przykład zaSecurityServiceImpl
pomocą metodygetCurrentPrincipal
implementującejSecurityService
interfejs, a następnie w testach możesz po prostu utworzyć makietę implementacji tego interfejsu, która zwraca żądaną jednostkę główną bez dostępu doSecurityContextHolder
.źródło