J'essayais de comprendre comment tester unitaire si mes URL de mes contrôleurs sont correctement sécurisées. Juste au cas où quelqu'un change les choses et supprime accidentellement les paramètres de sécurité.
Ma méthode de contrôleur ressemble à ceci:
@RequestMapping("/api/v1/resource/test")
@Secured("ROLE_USER")
public @ResonseBody String test() {
return "test";
}
J'ai mis en place un WebTestEnvironment comme ceci:
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();
}
}
Dans mon test actuel, j'ai essayé de faire quelque chose comme ceci:
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());
}
}
J'ai ramassé ceci ici:
- http://java.dzone.com/articles/spring-test-mvc-junit-testing ici:
- http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller ou ici:
- Comment JUnit teste une annotation @PreAuthorize et son ressort EL spécifié par un Spring MVC Controller?
Pourtant, si l'on regarde de près, cela n'aide que lorsque l'on n'envoie pas de requêtes réelles aux URL, mais uniquement lors du test des services au niveau de la fonction. Dans mon cas, une exception "accès refusé" a été levée:
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]
...
Il convient de noter que les deux messages de journal suivants indiquent essentiellement qu'aucun utilisateur n'a été authentifié, ce qui indique que le paramètre Principal
n'a pas fonctionné ou qu'il a été écrasé.
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
Réponses:
En cherchant une réponse, je n'ai pas trouvé de réponse facile et flexible à la fois, puis j'ai trouvé la référence de sécurité Spring et j'ai réalisé qu'il y avait des solutions presque parfaites. Les solutions AOP sont souvent les meilleures pour les tests, et Spring les fournit avec
@WithMockUser
,@WithUserDetails
et@WithSecurityContext
, dans cet artefact:Dans la plupart des cas,
@WithUserDetails
rassemble la flexibilité et la puissance dont j'ai besoin.Comment fonctionne @WithUserDetails?
Fondamentalement, il vous suffit de créer une personnalisation
UserDetailsService
avec tous les profils d'utilisateurs possibles que vous souhaitez tester. Par exempleMaintenant, nos utilisateurs sont prêts, alors imaginez que nous voulons tester le contrôle d'accès à cette fonction de contrôleur:
Ici , nous avons une fonction Obtenir mappé à la route / foo / salut et nous testons une sécurité basée sur les rôles avec l'
@Secured
annotation, bien que vous pouvez tester@PreAuthorize
et@PostAuthorize
aussi bien. Créons deux tests, l'un pour vérifier si un utilisateur valide peut voir cette réponse de salut et l'autre pour vérifier si elle est réellement interdite.Comme vous le voyez, nous avons importé
SpringSecurityWebAuxTestConfig
pour fournir à nos utilisateurs des tests. Chacun utilisé sur son cas de test correspondant simplement en utilisant une annotation simple, réduisant le code et la complexité.Mieux utiliser @WithMockUser pour une sécurité basée sur les rôles plus simple
Comme vous le voyez, vous disposez de
@WithUserDetails
toute la flexibilité dont vous avez besoin pour la plupart de vos applications. Il vous permet d'utiliser des utilisateurs personnalisés avec n'importe quel GrantedAuthority, comme des rôles ou des autorisations. Mais si vous travaillez uniquement avec des rôles, les tests peuvent être encore plus faciles et vous pouvez éviter de créer une personnalisationUserDetailsService
. Dans de tels cas, spécifiez une simple combinaison d'utilisateur, de mot de passe et de rôles avec @WithMockUser .L'annotation définit les valeurs par défaut pour un utilisateur très basique. Comme dans notre cas, la route que nous testons nécessite simplement que l'utilisateur authentifié soit un gestionnaire, nous pouvons cesser d'utiliser
SpringSecurityWebAuxTestConfig
et faire cela.Notez que maintenant, au lieu de l'utilisateur [email protected], nous obtenons la valeur par défaut fournie par
@WithMockUser
: utilisateur ; mais cela n'a pas d'importance car ce qui nous importe vraiment, c'est son rôle:ROLE_MANAGER
.Conclusions
Comme vous le voyez avec des annotations comme
@WithUserDetails
et@WithMockUser
nous pouvons basculer entre différents scénarios d'utilisateurs authentifiés sans créer de classes aliénées de notre architecture juste pour faire des tests simples. Il vous est également recommandé de voir comment @WithSecurityContext fonctionne pour encore plus de flexibilité.la source
tom
, tandis que la seconde est parjerry
?BasicUser
etManager User
donné dans la réponse. Le concept clé est qu'au lieu de nous soucier des utilisateurs, nous nous soucions réellement de leurs rôles, mais chacun de ces tests, situés dans le même test unitaire, représente en fait des requêtes différentes. effectué par différents utilisateurs (avec des rôles différents) sur le même point de terminaison.Depuis Spring 4.0+, la meilleure solution est d'annoter la méthode de test avec @WithMockUser
N'oubliez pas d'ajouter la dépendance suivante à votre projet
la source
Il s'est avéré que le
SecurityContextPersistenceFilter
, qui fait partie de la chaîne de filtres Spring Security, réinitialise toujours mySecurityContext
, que j'ai défini en appelantSecurityContextHolder.getContext().setAuthentication(principal)
(ou en utilisant la.principal(principal)
méthode). Ce filtre définit leSecurityContext
dans leSecurityContextHolder
avec un àSecurityContext
partir d'unSecurityContextRepository
OVERWRITING celui que j'ai défini précédemment. Le référentiel est unHttpSessionSecurityContextRepository
par défaut. LeHttpSessionSecurityContextRepository
inspecte le donnéHttpRequest
et essaie d'accéder au fichier correspondantHttpSession
. S'il existe, il essaiera de lire leSecurityContext
fichierHttpSession
. Si cela échoue, le référentiel génère un fichier videSecurityContext
.Ainsi, ma solution est de passer un
HttpSession
avec la requête, qui contient leSecurityContext
:la source
getPrincipal()
qui, à mon avis, est un peu trompeur. idéalement, il aurait dû être nommégetAuthentication()
. de même, dans votresignedIn()
test, la variable locale doit être nomméeauth
ou à laauthentication
place deprincipal
Ajoutez pom.xml:
et utiliser
org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
pour la demande d'autorisation. Consultez l'exemple d'utilisation à l' adresse https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ).Mettre à jour:
4.0.0.RC2 fonctionne pour spring-security 3.x. Pour spring-security 4 spring-security-test, faites partie de spring-security ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , la version est la même ).
La configuration est modifiée: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc
Exemple d'authentification de base: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .
la source
Voici un exemple pour ceux qui souhaitent tester Spring MockMvc Security Config à l'aide de l'authentification de base Base64.
Dépendance de Maven
la source
Réponse courte:
Après avoir effectué le
formLogin
test de sécurité du printemps, chacune de vos demandes sera automatiquement appelée en tant qu'utilisateur connecté.Longue réponse:
Vérifiez cette solution (la réponse est pour le printemps 4): Comment connecter un utilisateur avec le nouveau test mvc du printemps 3.2
la source
Options pour éviter d'utiliser SecurityContextHolder dans les tests:
SecurityContextHolder
utilisant une bibliothèque fictive - EasyMock par exempleSecurityContextHolder.get...
dans votre code dans un service - par exempleSecurityServiceImpl
avec une méthodegetCurrentPrincipal
qui implémente l'SecurityService
interface, puis dans vos tests, vous pouvez simplement créer une implémentation fictive de cette interface qui renvoie le principal souhaité sans accès àSecurityContextHolder
.la source