J'ai une application Spring Boot + Spring Security qui comporte plusieurs chemins antMatchers
; certains fullyAuthenticated()
, certains permitAll()
.
Comment écrire un test qui vérifie que SecurityConfiguration
a mes points de terminaison sous /api/**
(et finalement d’autres) sécurisés correctement?
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
//...
.antMatchers("/api/**").fullyAuthenticated()
}
}
Utiliser spring-boot-1.5.2.RELEASE
, spring-security-core-4.2.2-release
.
Clarification1: Je souhaite tester le SecurityConfiguration
aussi directement que possible, par opposition au test transitoire via l'un des points de terminaison /api/**
, qui peuvent avoir leur propre sécurité @PreAuthorize
.
Clarification2: Je voudrais quelque chose de semblable à ceci WebSecurityConfigurerAdapterTests .
Clarification3: Je voudrais @Autowire
quelque chose au niveau de la couche de sécurité Spring, idéalement HttpSecurity
, à tester.
Donc, vous voulez vous assurer que si quelqu'un change de .antMatchers("/api/**")
en .antMatchers("/WRONG_PATH/**")
, alors vous avez un test qui va le comprendre?
Les règles que vous définissez avec HttpSecurity
finiront par configurer une FilterChainProxy
avec un ou plusieurs SecurityFilterChain
, chacune avec une liste de filtres. Chaque filtre, tel que UsernamePasswordAuthenticationFilter
(Utilisé pour la connexion par formulaire), aura une RequestMatcher
définie dans la super classe AbstractAuthenticationProcessingFilter
. Le problème est que RequestMatcher
est une interface qui actuellement a 12 implémentations différentes, et ceci inclut AndRequestMatcher
et OrRequestMatcher
, la logique de correspondance n’est donc pas toujours simple. Et plus important encore, RequestMatcher
n’a qu’une méthode boolean matches(HttpServletRequest request)
, et souvent l’implémentation n’expose pas la configuration. Vous devrez donc utiliser la réflexion pour accéder aux configurations privées de chaque implémentation RequestMatcher
(qui pourrait changer dans le futur).
Si vous empruntez ce chemin et que vous passez auto FilterChainProxy
dans un test et que vous utilisez la réflexion pour procéder à un reverse engineering de la configuration, vous devez prendre en compte toutes les dépendances d'implémentation dont vous disposez. Par exemple, WebSecurityConfigurerAdapter
a une liste de filtres par défaut, qui peut changer d’une version à l’autre, et sauf désactivation, elle doit être définie explicitement pour chaque filtre. De plus, de nouveaux filtres et RequestMatchers
peuvent être ajoutés au fil du temps, ou la chaîne de filtres générée par HttpSecurity
dans une version de Spring Security peut être légèrement différente dans la version suivante (peut-être peu probable, mais toujours possible).
L'écriture d'un test générique pour votre configuration de sécurité Spring est techniquement possible, mais ce n'est pas vraiment une chose facile à faire, et les filtres Spring Security n'étaient certainement pas conçus pour supporter cela. J'ai beaucoup travaillé avec Spring Security depuis 2010 et je n'ai jamais eu besoin d'un tel test, et personnellement, je pense que ce serait une perte de temps d'essayer de le mettre en œuvre. Je pense que le temps sera beaucoup mieux dépensé pour écrire un framework de test qui facilite l'écriture de test d'intégrations, qui testera implicitement la couche de sécurité ainsi que la logique métier.
MockMVC devrait suffire à vérifier votre configuration de sécurité puisque la seule chose qu’elle se moque est la couche HTTP. Toutefois, si vous souhaitez vraiment tester votre application Spring Boot, votre serveur Tomcat et tout le reste, vous devez utiliser @SpringBootTest
, comme ceci.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class NoGoServiceTest {
@LocalServerPort
private int port;
private <T> T makeDepthRequest(NoGoRequest request, NoGoResponse response, String path, Class<T> responseClass) {
testService.addRequestResponseMapping(request, response);
RestTemplate template = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON));
headers.add("Authorization", "Bearer " + tokenProvider.getToken());
RequestEntity<NoGoRequest> requestEntity = new RequestEntity<>(request, headers, HttpMethod.POST, getURI(path));
ResponseEntity<T> responseEntity = template.exchange(requestEntity, responseClass);
return responseEntity.getBody();
}
@SneakyThrows(URISyntaxException.class)
private URI getURI(String path) {
return new URI("http://localhost:" +port + "/nogo" + path);
}
// Test that makes request using `makeDepthRequest`
}
Ce code fait partie d'un test tiré d'un projet open source ( https://github.com/maritime-web/NoGoService ). L'idée de base est de démarrer le test sur un port aléatoire, que Spring injectera ensuite dans un champ du test. Cela vous permet de construire des URL et d'utiliser Springs RestTemplate
pour envoyer une requête http au serveur, en utilisant les mêmes classes DTO que vos contrôleurs. Si le mécanisme d'authentification est de base ou un jeton, vous devez simplement ajouter l'en-tête d'autorisation approprié, comme dans cet exemple. Si vous utilisez l'authentification par formulaire, cela devient un peu plus difficile, car vous devez d'abord GET /login
, puis extraire le jeton CSRF et le cookie JSessionId, puis la POST
avec les informations d'identification à /login
. le nouveau cookie JSessionId, car sessionId est modifié après la connexion pour des raisons de sécurité.
J'espère que c'était ce dont vous aviez besoin.
Si vous souhaitez savoir par programme quels points de terminaison existent, vous pouvez insérer automatiquement la liste de RequestHandlerProvider
dans votre test et les filtrer en fonction du chemin sur lequel ils sont exposés.
@Autowired
List<RequestHandlerProvider> handlerProviders;
@Test
public void doTest() {
for (RequestHandlerProvider handlerProvider : handlerProviders) {
for (RequestHandler requestHandler : handlerProvider.requestHandlers()) {
for (String pattern : requestHandler.getPatternsCondition().getPatterns()) {
// call the endpoint without security and check that you get 401
}
}
}
}
L’utilisation de RequestHandlerProvider
permet à SpringFox de déterminer les points de terminaison disponibles et leur signature lorsqu’il crée la définition swagger d’une API.
Sauf si vous passez beaucoup de temps à créer la bonne entrée pour chaque noeud final, vous ne récupérerez pas 200 OK du noeud final en incluant un jeton de sécurité valide. Vous devrez donc probablement accepter 400 comme réponse correcte.
Si vous êtes déjà inquiet qu'un développeur commette des erreurs de sécurité lors de l'introduction d'un nouveau point de terminaison, je suis également préoccupé par la logique du point de terminaison. C'est pourquoi je pense que vous devriez avoir un test d'intégration pour chacun d'eux. votre sécurité aussi.
Je vois ci-dessous cas de test peut vous aider à réaliser ce que vous voulez. Il s'agit d'un test d'intégration permettant de tester la configuration de la sécurité Web. Nous avons effectué des tests similaires pour l'ensemble de notre code piloté par TDD.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@WebAppConfiguration
public class WebConfigIT {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private FilterChainProxy springSecurityFilterChain;
@Before
public void setup() throws Exception {
mockMvc = webAppContextSetup(webApplicationContext)
.addFilter(springSecurityFilterChain)
.build();
}
@Test
public void testAuthenticationAtAPIURI() throws Exception {
mockMvc.perform(get("/api/xyz"))
.andExpect(status.is3xxRedirection());
}
Cela ressemble toutefois à un test explicite du point final (ce qui est de toute façon un test à effectuer si vous utilisez TDD), mais cela ramène également la chaîne de filtres de sécurité Spring dans son contexte pour vous permettre de tester le contexte de sécurité de l'application.
Penser un peu en dehors de la boîte et répondre à la question différemment ne serait-il pas plus simple de définir simplement une chaîne statique [], par ex.
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String[] FULLY_AUTH_PUBLIC_URLS = {"/api/**", "/swagger-resources/**", "/health", "/info" };
protected void configure(HttpSecurity http) throws Exception {
http
//...
.antMatchers(FULLY_AUTH_PUBLIC_URLS).fullyAuthenticated()
}
}
...
Et ensuite, si le but du test est de s’assurer qu’aucun changement n’est apporté aux URL publiques, il suffit de tester la liste connue?
L'hypothèse retenue ici est que Spring Security fonctionne et a été testé. Par conséquent, la seule chose que nous testons est que la liste des URL publiques n'a pas été modifiée. S'ils ont changé, un test devrait échouer, soulignant pour le développeur qu'il y a des dragons qui changent ces valeurs? Je comprends que cela ne couvre pas les clarifications, mais en supposant que les URL publiques statiques fournies sont connues pour être précises, cette approche fournirait une unité de retour arrière testable si cela était nécessaire.