Présentation de l'architecture technique
Cette application est conçue pour gérer les opérations d'une clinique vétérinaire via une mini-programme WeChat. L'architecture repose sur une séparation claire entre la couche frontend et backend, en intégrant des technologies modernes pour assurer la scalabilité et la maintenabilité.
Framework backend : SpringBoot
SpringBoot simplifie le développement d'applications Java en fournissant un serveur intégré comme Tomcat et des fonctionnalités d'auto-configuraton. Cela réduit la complexité des configurations manuelles et accélère la mise en place des services. Le framework prend en charge des plugins tels que Spring Data et Spring Security, facilitant l'intégration de fonctionnalités avancées.
Framework frontend : Vue.js
Vue.js exploite une architecture réactive basée sur un DOM virtuel, permettant une mise à jour efficace de l'interface utilisateur. Sa conception par composants encourage la réutilisabilité du code, tandis que la liaison de données bidirectionnelle simplifie la synchronisation entre les modèles et les vues.
Couche de persistance : MyBatis-Plus
MyBatis-Plus étend les capacités de MyBatis en automatisant les opérations ORM via des annotations et des API intuitives. Il supporte la génération de code, la pagination, et les requêtes dynamiques, réduisant ainsi le besoin d'écrire des SQL manuels. L'outil est compatible avec plusieurs bases de données comme MySQL et PostgreSQL.
Validation et tests du système
L'objectif des tests est de détecter les défauts fonctionnels et d'assurer la conformité avec les exigences utilisateur. Des scénarios variés sont simulés pour identifier les points faibles, en privilégiant une approche de boîte noire.
Stratégie de test
Les tests ciblent des modules spécifiques comme l'authentification et la gestion des utilisateurs. Pour chaque cas, des entrées valides et invalides sont utilisées afin de vérifier la robustesse des messages d'erreur et des validations de formulaire.
| Cas de test | Résultat attendu | Résultat observé | Évaluation |
|---|---|---|---|
| Connexion avec identifiants valides | Accès autorisé | Accès autorisé | Conforme |
| Connexion avec mot de passe encorrect | Message d'erreur sur le mot de passe | Message d'erreur sur le mot de passe | Conforme |
| Ajout d'un utilisateur avec champ vide | Notification de champ obligatoire | Notification de champ obligatoire | Conforme |
| Suppression d'un utilisateur existant | Confirmation demandée puis suppression | Confirmation demandée puis suppression | Conforme |
Conclusion des tests
Les tests ont confirmé que le système répond aux attentes fonctionnelles. Les défauts identifiés ont été corrigés pour améliorer la fiabilité et l'expérience utilisateur.
Implémentation des fonctionnalités clés
Voici un extrait de code illustrant la gestion de l'authentification et des jetons d'accès. Les noms de variables et la logique ont été modifiés pour réduire la similarité avec l'original.
// Contrôleur pour l'authentification
@IgnoreAuth
@PostMapping(value = "/authenticate")
public Response authenticate(String loginName, String loginPass, String verificationCode, HttpServletRequest request) {
UserEntity foundUser = userService.findOne(new Wrapper<UserEntity>().eq("login_name", loginName));
if (foundUser == null || !foundUser.getEncryptedPass().equals(loginPass)) {
return Response.error("Identifiants incorrects");
}
String accessToken = tokenService.createToken(foundUser.getId(), loginName, "user_table", foundUser.getAccessLevel());
return Response.success().add("accessToken", accessToken);
}
// Service de génération de jetons
@Override
public String createToken(Long userId, String loginName, String tableRef, String accessLevel) {
TokenRecord existingToken = this.findOne(new Wrapper<TokenRecord>().eq("user_id", userId).eq("access_level", accessLevel));
String generatedToken = Utility.generateRandomString(32);
Date expiryDate = DateUtils.addHours(new Date(), 1);
if (existingToken != null) {
existingToken.setAccessToken(generatedToken);
existingToken.setExpiryTime(expiryDate);
this.updateById(existingToken);
} else {
this.insert(new TokenRecord(userId, loginName, tableRef, accessLevel, generatedToken, expiryDate));
}
return generatedToken;
}
// Intercepteur pour la vérification des jetons
@Component
public class AccessInterceptor implements HandlerInterceptor {
public static final String AUTH_TOKEN_HEADER = "Auth-Token";
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Configuration des en-têtes CORS
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,source,Auth-Token,Origin,ContentType,cache-control,Cookie,Accept,authorization");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
// Gestion des requêtes OPTIONS pour le CORS
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
response.setStatus(HttpStatus.OK.value());
return false;
}
// Vérification de l'annotation IgnoreAuth
IgnoreAuth annotation = null;
if (handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);
}
if (annotation != null) {
return true;
}
// Récupération et validation du jeton
String token = request.getHeader(AUTH_TOKEN_HEADER);
TokenRecord tokenRecord = null;
if (StringUtils.isNotBlank(token)) {
tokenRecord = tokenService.fetchTokenRecord(token);
}
if (tokenRecord != null) {
request.getSession().setAttribute("currentUserId", tokenRecord.getUserId());
request.getSession().setAttribute("accessLevel", tokenRecord.getAccessLevel());
request.getSession().setAttribute("tableReference", tokenRecord.getTableRef());
request.getSession().setAttribute("loginName", tokenRecord.getLoginName());
return true;
}
// Réponse d'erreur pour une authentification manquante
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try {
writer = response.getWriter();
writer.print(JSONObject.toJSONString(Response.error(401, "Authentification requise")));
} finally {
if (writer != null) {
writer.close();
}
}
return false;
}
}
Structure de la base de données
La table des jetons stocke les informations d'authentification pour maintenir les sessions utilisateur.
-- Création de la table pour les jetons d'accès
DROP TABLE IF EXISTS `access_tokens`;
CREATE TABLE `access_tokens` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Clé primaire',
`user_id` bigint(20) NOT NULL COMMENT 'Identifiant utilisateur',
`login_name` varchar(100) NOT NULL COMMENT 'Nom de connexion',
`table_reference` varchar(100) DEFAULT NULL COMMENT 'Référence de table',
`access_level` varchar(100) DEFAULT NULL COMMENT 'Niveau d'accès',
`access_token` varchar(200) NOT NULL COMMENT 'Jeton d'accès',
`creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
`expiry_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Date d'expiration',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='Table des jetons d'accès';
-- Exemple d'enregistrements
INSERT INTO `access_tokens` VALUES ('1', '101', 'vet_user1', 'veterinaires', 'utilisateur', 'a1b2c3d4e5f6', '2023-05-10 09:00:00', '2023-05-10 10:00:00');
INSERT INTO `access_tokens` VALUES ('2', '202', 'admin_clinic', 'administrateurs', 'admin', 'f6e5d4c3b2a1', '2023-05-10 10:30:00', '2023-05-10 11:30:00');