Introducción
Este proyecto implementa un sistema de control de acceso para comunidades residenciales, desarrollado con un stack tecnológico moderno que incluye Spring Boot en el backend, Vue.js en el frontend y uniapp para aplicaciones móviles. El sistema permite gestionar el flujo de residentes y visitantes, registranod entradas y salidas con validación de identidad.
Stack Tecnológico
Backend: Spring Boot
Spring Boot simplifica la creación de aplicaciones Java al incorporar servidores embebidos como Tomcat o Undertow, eliminando la necesidad de configuraciones manuales complejas. Su mecanismo de autoconfiguración detecta automáticamente las dependencias del proyecto y ajusta los parámetros correspondientes. Además, integra módulos como Spring Data JPA, Spring Security y Spring Cloud, permitiendo construir aplicaciones robustas y escalables con menor esfuerzo de configuración.
Frontend: Vue.js
Vue.js emplea un DOM virtual en memoria para optimizar las operaciones de renderizado. Mediante enlace reactivo de datos, cualquier modificación en el modelo se refleja automáticamente en la interfaz, liberando al desarrollador de actualizaciones manuales. Su arquitectura basada en componentes facilita la reutilización de código y el mantenimiento, ofreciendo un desarrollo ágil y estructurado.
Capa de Persistencia: MyBatis-Plus
MyBatis-Plus extiende las capacidades de MyBatis con herramientas adicionales que reducen el código repetitivo. Soporta múltiples motores de base de datos como MySQL, PostgreSQL, Oracle y SQL Server. Entre sus características destacan: generador automático de entidades y mappers, paginación integrada, consultas dinámicas mediante wrappers, control de concurrencia optimista y análisis de rendimiento SQL. Estos elementos agilizan significativamente la implementación de la capa de acceso a datos.
Pruebas del Sistema
Objetivos de Prueba
La fase de pruebas busca identificar defectos desde múltiples perspectivas antes del despliegue. Se realizan pruebas funcionales mediante caja negra, simulando escenarios reales de uso para verificar que cada módulo cumpla con los requisitos especificados. El proceso garantiza la calidad final, detectando inconsistencias entre el comportamiento esperado y el resultado real, mejorando la experiencia del usuario final.
Pruebas Funcionales
Autenticación de Usuarios
El módulo de inicio de sesión valida credenciales contra la base de datos y verifica el código captcha. También controla los permisos según el rol del usuario.
| Datos de Entrada | Resultado Esperado | Resultado Real | Análisis |
|---|---|---|---|
| usuario: admin, clave: 123456, captcha: válido | Acceso concedido | Acceso concedido | Correcto |
| usuario: admin, clave: 999999, captcha: válido | Clave incorrecta | Mensaje de clave incorrecta | Correcto |
| usuario: admin, clave: 123456, captcha: inválido | Captcha incorrecto | Mensaje de captcha inválido | Correcto |
| usuario: vacío, clave: 123456, captcha: válido | Usuario obligatorio | Mensaje de campo requerido | Correcto |
Gestión de Residentes
Las operaciones de administración incluyen crear, modificar, eliminar y buscar residentes. Se validan campos obligatorios, duplicados y confirmaciones de borrado.
| Datos de Entrada | Resultado Esperado | Resultado Real | Análisis |
|---|---|---|---|
| Datos completos del residente | Registro exitoso | Residente aparece en el listado | Correcto |
| Modificación de datos existentes | Actualización correcta | Datos modificados visibles | Correcto |
| Eliminar residente seleccionado | Confirmación y eliminación | Residente removido del listado | Correcto |
| Registro sin nombre | Error de validación | Mensaje: nombre requerido | Correcto |
| Nombre duplicado | Inserción rechazada | Mensaje: nombre ya existe | Correcto |
Conclusión de Pruebas
Mediante pruebas de caja negra se verificó que todos los módulos cumplen con los requisitos funcionales. Los escenarios simulados confirmaron que la lógica de negocio opera correctamente y la interfaz responde adecuadamente ante entradas válidas e inválidas. El sistema demostró estabilidad y usabilidad aceptable para su propósito.
Implementación de Código
Controlador de Autenticación
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AccountService accountService;
@Autowired
private SessionTokenService sessionTokenService;
@PostMapping("/signin")
public ResponseEntity<Map<String, Object>> authenticate(
@RequestParam String loginName,
@RequestParam String secret,
@RequestParam String captchaCode,
HttpServletRequest req) {
Account account = accountService.findByLoginName(loginName);
if (account == null || !account.getSecret().equals(secret)) {
return ResponseEntity.status(401).body(Map.of("msg", "Credenciales inválidas"));
}
String authToken = sessionTokenService.createOrRefreshToken(
account.getId(), loginName, "accounts", account.getRoleName());
return ResponseEntity.ok(Map.of("authToken", authToken));
}
}
Servicio de Tokens de Sesión
@Service
public class SessionTokenService {
@Autowired
private SessionTokenDao sessionTokenDao;
public String createOrRefreshToken(Long accountId, String loginName,
String sourceTable, String role) {
SessionToken existing = sessionTokenDao.findByAccountAndRole(accountId, role);
String randomToken = TokenGenerator.generateRandom(32);
Date expiration = computeExpiration(1);
if (existing != null) {
existing.setAuthToken(randomToken);
existing.setExpiresAt(expiration);
sessionTokenDao.update(existing);
} else {
SessionToken newToken = new SessionToken();
newToken.setAccountId(accountId);
newToken.setLoginName(loginName);
newToken.setSourceTable(sourceTable);
newToken.setRole(role);
newToken.setAuthToken(randomToken);
newToken.setExpiresAt(expiration);
sessionTokenDao.insert(newToken);
}
return randomToken;
}
private Date computeExpiration(int hoursAhead) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY, hoursAhead);
return calendar.getTime();
}
}
Interceptor de Autorización
@Component
public class TokenAuthInterceptor implements HandlerInterceptor {
private static final String AUTH_HEADER = "Authorization";
@Autowired
private SessionTokenService sessionTokenService;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp,
Object handler) throws Exception {
configureCorsHeaders(req, resp);
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
resp.setStatus(HttpServletResponse.SC_OK);
return false;
}
SkipAuth skipAnnotation = extractSkipAnnotation(handler);
if (skipAnnotation != null) {
return true;
}
String bearerToken = req.getHeader(AUTH_HEADER);
SessionToken tokenData = null;
if (StringUtils.hasText(bearerToken)) {
tokenData = sessionTokenService.validateToken(bearerToken);
}
if (tokenData != null) {
HttpSession session = req.getSession();
session.setAttribute("currentUserId", tokenData.getAccountId());
session.setAttribute("currentUserRole", tokenData.getRole());
session.setAttribute("sourceTable", tokenData.getSourceTable());
session.setAttribute("currentLoginName", tokenData.getLoginName());
return true;
}
sendUnauthorizedResponse(resp);
return false;
}
private void configureCorsHeaders(HttpServletRequest req, HttpServletResponse resp) {
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
resp.setHeader("Access-Control-Max-Age", "3600");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Headers",
"x-requested-with, Authorization, Content-Type, Accept, Cookie");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
}
private SkipAuth extractSkipAnnotation(Object handler) {
if (handler instanceof HandlerMethod) {
return ((HandlerMethod) handler).getMethodAnnotation(SkipAuth.class);
}
return null;
}
private void sendUnauthorizedResponse(HttpServletResponse resp) throws IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json; charset=utf-8");
resp.setStatus(401);
resp.getWriter().write("{\"code\": 401, \"message\": \"Debe iniciar sesión primero\"}");
}
}
Esquema de Base de Datos
-- Estructura de tabla: session_tokens
DROP TABLE IF EXISTS `session_tokens`;
CREATE TABLE `session_tokens` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'Clave primaria',
`account_id` BIGINT(20) NOT NULL COMMENT 'Identificador de cuenta',
`login_name` VARCHAR(100) NOT NULL COMMENT 'Nombre de acceso',
`source_table` VARCHAR(100) DEFAULT NULL COMMENT 'Tabla de origen',
`role` VARCHAR(100) DEFAULT NULL COMMENT 'Rol asignado',
`auth_token` VARCHAR(200) NOT NULL COMMENT 'Token de autenticación',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Fecha de creación',
`expires_at` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Fecha de expiración',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_account_role` (`account_id`, `role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='Tokens de sesión';
-- Registros de ejemplo
INSERT INTO `session_tokens` VALUES
(1, 1, 'admin', 'accounts', 'administrador', 'a7f3k9x2m8n4q1w6e5r7t0y3u2i8o9p5', '2024-01-10 08:30:00', '2024-01-10 09:30:00'),
(2, 15, 'residente01', 'accounts', 'residente', 'b8g4l0y3n9o5r2x7e6s8t1z4u3j9p0q6', '2024-01-12 14:15:00', '2024-01-12 15:15:00'),
(3, 22, 'guardia01', 'accounts', 'seguridad', 'c9h5m1z4o0p6s3x8e7t9u2a5v4k0q1r7', '2024-01-15 19:45:00', '2024-01-15 20:45:00');