Ceci est une suite des articles que j'ai écrits dans When. J'étais accro à l'écriture de code de test pour le traitement lié à la connexion, je vais donc le laisser comme un mémorandum. De plus, l'explication du processus de connexion lui-même est décrite dans ici, donc si cela ne vous dérange pas.
Si vous avez des erreurs, faites-le moi savoir (╹◡╹)
En lisant cet article, vous comprendrez (probablement) ce qui suit:
L'environnement est le suivant. Les bibliothèques dépendantes, etc. sont les mêmes que celles de l'article précédent.
Vous pouvez également trouver le Code source sur GitHub.
L'application de connexion est simple avec les fonctions suivantes.
Un schéma approximatif de ceci est montré à la Fig.1.
Figure 1. Flux de traitement
La fonction en elle-même est simple, mais si vous prenez ceci et cela en considération, le nombre de packages augmentera et il sera difficile de saisir l'image dans son ensemble, donc la figure 2 montre ceux qui sont légèrement classés par rôle. ..
Figure 2. Vue d'ensemble du package
La chose la plus importante à garder à l'esprit lors des tests, qu'ils soient codés ou non, est d'être clair sur ce que vous voulez vérifier.
Cette fois, cela signifie que la fonction définie dans Spring Security fonctionne comme prévu
.
C'est raide et un peu difficile à saisir l'atmosphère, alors rendons-le un peu plus collant.
Comme le montre la figure 3, si vous dites «Faites-vous de votre mieux comme Spring Security l'a demandé», je pense que ce sera plus facile à imaginer.
Figure 3. Cible de vérification Spring Security
~~ Les personnages sont sales, donc ~~ C'est important, alors montrons également la figure ci-dessus dans une liste.
Ci-dessous, nous verrons comment écrire du code de test pour vérifier qu'il fonctionne correctement.
Commençons par un simple écran de connexion. L'image de l'écran doit ressembler à la figure 4.
Figure 4. Écran de connexion
Pour vérifier que le processus de connexion fonctionne correctement, vous devez tester les éléments suivants:
En d'autres termes, cela peut être exprimé comme «Spring Security ne passe-t-il que les gens comme prévu»?
Maintenant, vérifions si Spring Security fera réellement le travail.
Tout d'abord, en tant que cas simple, si vous vous connectez en tant qu'utilisateur existant sur la base de données, nous vérifierons si vous pouvez vous connecter. SpringSecurity exécute ce qui suit en tant que processus de connexion.
Le code du processus lui-même est expliqué dans un autre article, je vais donc l'omettre ici et regarder le code de test immédiatement.
LoginControllerTest.java
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class,
})
@AutoConfigureMockMvc
@SpringBootTest(classes = {LoginUtApplication.class})
@Transactional
public class LoginControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
Vous pouvez vous connecter en tant qu'utilisateur qui existe sur void DB() throws Exception {
this.mockMvc.perform(formLogin("/sign_in")
.user("top_user")
.password("password"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/top_user/init"));
}
}
J'ai écrit le code de test à peu près dans l'article précédent, donc ici je vais aborder les nouveaux éléments.
formLogin
En tant qu'argument à passer à la méthode perfromde
mockMvc, formLogin
est spécifié.
Le processus de connexion étant familier, vous pouvez imaginer l'opération dans l'atmosphère, mais les points importants ici sont les suivants.
Si vous comprenez le processus de connexion dans une certaine mesure, vous ne devriez pas être trébuché.
Comme indiqué par Officiel, Spring Security travaille pour vous rediriger vers l'URL spécifiée lors de connexions réussies / infructueuses. Par conséquent, ici, nous vérifierons si l'URL de la destination de la redirection est celle attendue. Comme je le reviendrai plus tard, il suffit que vous compreniez que si la connexion réussit, vous serez redirigé vers l'URL qui ressemble à l'écran supérieur de l'utilisateur.
Dans le code de test ci-dessus, il s'avère que Spring Security passe par l'utilisateur. Cependant, cela seul laisse la possibilité que n'importe qui soit le bienvenu et en sécurité. Ici, nous vérifierons que la sécurité est assurée, c'est-à-dire si les personnes qui ne veulent pas passer sont passées ou non.
Ce qui suit est un extrait du code de test cible.
LoginControllerTest(Extrait)
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
void Si un utilisateur de la base de données a le mauvais mot de passe, il sera redirigé vers l'écran d'échec.() throws Exception {
this.mockMvc.perform(formLogin("/sign_in")
.user("top_user")
.password("wrongpassword"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/?error"));
}
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
void Si vous vous connectez en tant qu'utilisateur qui n'existe pas sur la base de données, vous serez redirigé vers l'URL d'erreur.() throws Exception {
this.mockMvc.perform(formLogin("/sign_in")
.user("nobody")
.password("password"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/?error"));
}
Il n'y a pas de grande différence avec le système normal, mais le fait est que la destination de la redirection est «/? Error». Si cela fonctionne correctement, vous pouvez vérifier que non seulement les utilisateurs connectés peuvent se connecter, mais que les utilisateurs qui ne devraient pas pouvoir se connecter ne parviennent pas à se connecter.
Il s'agit d'un niveau minimal, mais j'ai pu écrire un code de test pour le processus de connexion. Les couches Dao et Service chevauchent l'article précédent, je vais donc les omettre. Si vous êtes intéressé, veuillez vous référer au code source.
À propos, l'application Web ne s'arrête pas après la connexion, mais utilise divers écrans pendant la connexion. Cependant, il est très gênant de se connecter au code de test de l'écran qui nécessite une connexion à chaque fois, puis d'écrire le code de test de l'écran.
Pour résoudre ce problème, ce serait très pratique si vous pouviez créer manuellement un «état journalisé». Maintenant, avant de regarder le code de test à l'écran après la connexion, voyons comment créer un état de connexion.
Eh bien, j'ai écrit que vous devriez créer un état connecté
, mais que devez-vous faire spécifiquement?
En tant que pré-processus, émettez un ID de session, créez un objet utilisateur à lier et faites-le sous une forme que Spring Security peut gérer ... et créez-le même si vous reproduisez le processus que Spring Security effectue un par un. Vous pouvez, mais cela semble un peu difficile.
En fait, il est traité par annotation comme introduit dans Official Vous pouvez créer un état de connexion simulé en écrivant simplement un peu. Ici, l '«état journalisé» est plus précisément appelé «SecurityContext». Pour une description du contexte, ici devrait être utile.
Soudain, l'histoire est devenue abstraite. Pour faciliter l'imagination, la figure 5 montre une représentation graphique simple du contexte.
Figure 5. Contexte de sécurité
Le contexte vous permet de `code créer le même état que lorsque vous avez cliqué sur l'écran. C'est pratique.
Regardons le code pour créer réellement le contexte.
Tout d'abord, voyons comment l'état de connexion est décrit dans le code de test avant d'entrer dans le contenu. L'écriture du code que je vais aborder facilitera l'écriture du code de test, et si vous en connaissez d'abord les mérites, ce sera plus facile à comprendre.
Exemple de code de test utilisant l'état de connexion
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
Le haut de l'utilisateur est passé en tant que vue dans le traitement d'init.() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(view().name("top_user"));
}
L'important est l'annotation WithMockCustomUser
. Passez le nom d'utilisateur et le mot de passe comme paramètres. Avec juste cela, en tant qu'utilisateur connecté, vous pouvez exécuter le code de test pour les écrans qui vous obligent à vous connecter. Je vous remercie.
À propos, cette annotation est une annotation personnalisée et vous devez travailler un peu dur pour la créer, mais une fois que vous l'avez créée, vous pouvez la réutiliser dans des applications qui nécessitent une connexion. Faisons de notre mieux pour vous faciliter la tâche.
Le premier est le code de l'annotation WithMockCustomUser
qui a été écrite précédemment.
Pour les annotations personnalisées, accédez à ici.
WithMockCustomUser.java
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username();
String password();
}
L'annotation elle-même est simple avec deux champs, mais une annotation avec un paramètre très long appelé WithSecurityContext
a une atmosphère difficile.
Ceci est une annotation pour définir SpringSecurityContext
. Cela ne sort pas très bien, donc pour le dire autrement, c'est comme un mémo pour Spring Security de se souvenir à l'avance de l'information "Souviens-toi de cette personne !!".
Comme son nom l'indique, WithMockCustomUser définit uniquement les informations utilisateur, jetons donc un coup d'œil au processus qui crée le contexte.
Même si cela s'appelle une fabrique de contexte, c'est assez rafraîchissant, alors jetons un coup d'œil au code réel.
WithMockCustomUserSecurityContextFactory.java
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser>{
@Autowired
private AuthenticationManager authenticationManager;
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
//Émettre des jetons pour l'authentification avec le nom d'utilisateur et le mot de passe
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUser.username(), customUser.password());
//Processus de connexion
Authentication auth = authenticationManager.authenticate(authToken);
context.setAuthentication(auth);
return context;
}
}
J'ai écrit beaucoup de code, mais c'est assez simple à faire, créer un état journalisé
ou SecurityContext.
Les éléments suivants sont approximativement exécutés en tant que processus individuels.
SecurityContextHolder est un objet pour gérer le contexte, et il gère diverses choses autour des threads, mais il est en dehors de la ligne principale, donc je l'omettrai cette fois. Je pense qu'il n'y a pas de problème si vous pensez que c'est à l'origine de la création du contexte.
Et, surtout, le «jeton d'authentification» ici est complètement différent de ce qui est souvent comparé aux cookies ou appelés jetons API. Cela semble avoir une signification plus proche de "Hard Token" car le nom d'utilisateur et le mot de passe sont les clés d'authentification.
C'est un peu hors sujet, mais en définissant une annotation personnalisée, vous pouvez facilement créer un état de connexion, c'est-à-dire SecurityContext
en spécifiant le nom d'utilisateur et le mot de passe sur le code de test. Tu l'as fait.
C'était un peu difficile, mais maintenant que vous pouvez créer un état de connexion, vous pouvez écrire un code de test pour les écrans qui nécessitent une connexion beaucoup plus facile.
Jetons maintenant un coup d'œil à l'écran supérieur des utilisateurs comme exemple d'écran nécessitant une connexion. L'image de l'écran est illustrée à la figure 6.
Figure 6. Écran supérieur pour les utilisateurs
C'est un écran simple qui salue simplement l'utilisateur. C'est un exemple d'écran, alors ne vous inquiétez pas du design ...
En passant, cette fois, le but est de vérifier le traitement lié à la connexion, donc pour cet écran, nous vérifierons si les éléments suivants fonctionnent comme prévu.
Comme pour l'écran de connexion, il semble bon de vérifier en fonction de "si vous pouvez faire quelque chose de mal". Regardons maintenant le code de test réel.
Comme pour le processus de connexion, nous examinerons d'abord l'opération sur l'itinéraire normal. Le nombre d'annotations que j'ai introduites plus tôt a augmenté, donc je pense que vous pouvez le comprendre rapidement.
TopUserControllerTest.java
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class,
WithSecurityContextTestExecutionListener.class
})
@AutoConfigureMockMvc
@SpringBootTest(classes = {LoginUtApplication.class})
@Transactional
public class TopUserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
Le haut de l'utilisateur est passé en tant que vue dans le traitement d'init.() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(view().name("top_user"));
}
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
Le nom d'utilisateur de connexion est transmis au modèle dans le processus d'initialisation void() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(model().attribute("loginUsername", "top_user"));
}
}
Comme pour WithMockCustomUser
, comme mentionné ci-dessus, en spécifiant le nom d'utilisateur et le mot de passe, l'état de connexion est créé.
Ici, vous pouvez voir que WithSecurityContextTestExecutionListener
, une classe qui semble être liée, est spécifiée dans TestExecutionListeners
.
Il s'agit d'un paramètre qui permet à «TestContextManager» de gérer «SecurityContext». Ecrire un contexte pour un contexte peut être un peu déroutant, alors organisons-le ici également. La figure 7 montre une représentation approximative de TestContextManager.
Figure 7. TestContextManager
Outre le contexte, il est également responsable du prétraitement et du post-traitement, mais le point important ici est qu'il gère le contexte nécessaire à l'exécution du test. Il peut être difficile de saisir l'image au premier coup d'œil, mais il est nécessaire de créer un état de connexion à l'avance avant d'exécuter le test, il est donc bon d'en être conscient.
Ensuite, voyons si nous pouvons empêcher "les gens qui complotent de mauvaises choses". Maintenant, pour faire le tri, regardons à nouveau ce que signifie la «mauvaise chose».
Puisque l'implémentation elle-même pour empêcher ce qui précède est définie dans la classe Config de Spring Security, vérifions ici si elle peut vraiment être empêchée en envoyant une demande de test
.
TopUserControllerTest.java(Extrait)
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
void Les utilisateurs non enregistrés ne peuvent pas passer à l'écran supérieur de l'utilisateur en tapant directement l'URL() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/"));
}
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="admin_user", password="password")
void Les utilisateurs avec des privilèges d'administrateur ne peuvent pas passer à l'écran supérieur de l'utilisateur en tapant directement l'URL.() throws Exception {
this.mockMvc.perform(get("/top_user/init"))
.andExpect(status().isForbidden());
}
Le problème est de savoir quoi faire si vous évitez les mauvaises choses.
Pour les utilisateurs qui ne sont pas connectés, nous voulons qu'ils `` redirigent vers l'écran de connexion '', cela décrit donc que le code d'état est redirigé (commençant par 3) et que la destination de la transition est l'écran de connexion.
Et dans le cas d'un utilisateur non autorisé (il semble plus naturel pour l'administrateur de le voir ...), il indique que 403 Forbidden sera retourné comme code de statut.
Ces codes de test montrent que Spring Security fait le travail qu'il était censé faire. Enfin, en post-traitement, j'aimerais jeter un œil à la déconnexion.
Processus de déconnexion
@Test
@DatabaseSetup(value = "/controller/top/setUp/")
@WithMockCustomUser(username="top_user", password="password")
void Transition vers l'écran de connexion par traitement de déconnexion() throws Exception {
this.mockMvc.perform(post("/logout")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/?logout"));
}
Le processus de déconnexion envoie également simplement une demande POST à l'URL de déconnexion définie dans Spring Security, afin que vous puissiez le comprendre dans une atmosphère.
Cependant, contrairement à la méthode formLogin
, une requête POST normale n'accorde pas de jeton CSRF. Par conséquent, vous devez ajouter le jeton CSRF à la requête avec la méthode wirh (SecurityMockMvcRequestPostProcessors.csrf)
.
Cela seul ne semble pas être un problème pour vérifier que le processus de déconnexion fonctionne, mais allons plus loin. Si vous vous déconnectez, vous pouvez dire que vous êtes passé à «non connecté». Par conséquent, l'opération correcte est que vous ne pouvez pas passer à un écran qui nécessite une connexion après la déconnexion.
Je voudrais également y jeter un œil, au cas où. Cela dit, ce n'est pas difficile, je viens d'ajouter le traitement de la transition d'écran après la déconnexion. Si vous regardez les figures 8 et 9 ci-dessous, vous pouvez voir que le processus de déconnexion a créé un état dans lequel vous n'êtes pas connecté.
Figure 8. Après la déconnexion, elle échoue car elle a été effectuée avant la déconnexion.
Figure 9. Vous serez invité à vous reconnecter après vous être déconnecté
Cela fait un peu long, mais je peux maintenant vérifier le processus de connexion avec le code de test. Je l'ai fait. Avant d'entrer dans le résumé, ce sera un peu différent du processus de connexion, mais je voudrais aborder brièvement le «processus d'enregistrement de l'utilisateur» qui en est accro en tant que supplément.
Maintenant, en ce qui concerne le processus d'enregistrement des utilisateurs, le processus lui-même est presque le même que la création d'un SecurityContext
.
Voir Code source pour l'implémentation.
Ce que nous voulons couvrir ici, c'est le processus de validation. L'image du mouvement est illustrée à la Fig.10.
Figure 10. Enregistrement de l'utilisateur
Je suis accro à la façon d'écrire le code de test pour le traitement de validation, alors j'écrirai les points sous forme de mémorandum.
Code de test du processus de validation
@Test
Une erreur AuthInpuType void se produit lors du POST d'un groupe de symboles autres que le trait de soulignement alphanumérique demi-largeur() throws Exception {
this.mockMvc.perform(post("/signup/register")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "<script>alert(1)</script>")
.param("rawPassword", ";delete from ut_user")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrorCode("userForm", "username", "AuthInputType"))
.andExpect(model().attributeHasFieldErrorCode("userForm", "rawPassword", "AuthInputType"));
}
@DatabaseSetup(value = "/controller/signup/setUp/")
@Test
Une erreur de nom d'utilisateur unique se produit lors du POST avec un nom d'utilisateur qui existe déjà dans la base de données vide() throws Exception {
this.mockMvc.perform(post("/signup/register")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "test_user")
.param("rawPassword", "password")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrorCode("userForm", "username", "UniqueUsername"));
}
C'est écrit beaucoup, mais tout ce que vous faites est de définir les valeurs et d'envoyer manuellement la requête POST. L'important ici est la méthode ʻattributeHasFieldErrorCode. Nous transmettons
nom, fieldName, error` comme arguments.
Chacun d'eux a la correspondance suivante.
En ce qui concerne le nom et le fieldName, si vous avez touché l'application MVC, vous serez surpris par l'image. Quant à l'erreur, vous pouvez la voir en regardant réellement le champ.
UserForm.java
@Data
public class UserForm {
@AuthInputType
@UniqueUsername
private String username;
@AuthInputType
private String rawPassword;
}
Ici, ce qui est passé à l'argument comme erreur correspond au nom de l'annotation.
L'annotation spécifiée est une annotation personnalisée, et puisque l'annotation «Contrainte» est ajoutée, elle se comporte comme une «annotation de contrainte».
S'il y a une violation de contrainte, BindingResult
la détectera comme une erreur, afin qu'elle puisse être gérée par le processus de validation.
Pour plus d'informations, veuillez consulter Officiel.
Je n'ai pas bien compris le contrôle de corrélation, et j'étais confus par le code de test de correspondance du mot de passe, donc si quelqu'un le connaît, je vous serais reconnaissant de bien vouloir fournir des informations. (::
Concernant le processus de connexion, bien que ce soit une idée approximative, il est maintenant possible d'écrire du code de test et de vérifier l'opération. En regardant le CRUD simple dans l'article précédent et l'important processus de connexion dans l'application Web de cet article, je pense que s'il s'agit d'une application simple, vous pouvez la développer tout en écrivant fermement le code de test.
Quand j'écris du code, j'arrive soudainement à un refactoring super sympa et je veux changer le code, mais si j'écris du code de test comme celui-ci, «le comportement vérifié est-il cassé?» Est-ce qu'un bouton. Vous pourrez le vérifier dès le début.
De plus, vous serez en mesure d'exécuter des tests efficacement et avec une reproductibilité garantie sans avoir à vous connecter à chaque fois et à cliquer sur l'écran pour afficher un écran d'erreur. Cela coûte cher d'apprendre à écrire du code de test, mais cela vous donnera plus de temps pour écrire du code amusant, donc je pense que c'est une bonne idée de l'incorporer petit à petit tout en vous amusant.
Ella Je le dis, mais j'étudie toujours le code de test, alors j'aimerais faire de mon mieux pour étudier davantage.
Recommended Posts