A construção de um sistema de gestão de campus inteligente exige uma arquitetura robusta e escalável. Este artigo detalha a stack tecnológica, os processos de validação e a implementação prática de autenticação e controle de acesso para uma plataforma educacional moderna.
Stack Tecnológica
Backend com Spring Boot
O Spring Boot atua como a espinha dorsal do servidor, eliminando a necessidade de configurações manuais extensas de servidores de aplicação como Tomcat ou Undertow. Seu mecanismo de autoconfiguração analisa as dependências do projeto e ajusta o ambiente automaticamente. Além disso, a integração nativa com o ecossistema Spring (como Spring Security e Spring Data) acelera o desenvolvimento de APIs RESTful seguras e de alta performance.
Frontend com Vue.js
Para a interface do usuário, o Vue.js oferece uma abordagem reativa baseada em Virtual DOM. Essa abstração permite que o framework otimize as atualizações da interface, modificando apenas os nós necessários quando o estado da aplicação é alterado. A arquitetura baseada em componentes promove a reutilização de código e facilita a manutanção de aplicações complexas, como painéis administrativos e portais de estudantes.
Camada de Persistência com MyBatis-Plus
O MyBatis-Plus é utilizado para abstrair e simplificar as operações de banco de dados. Ele estende o MyBatis tradicional, fornecendo um CRUD genérico que reduz drasticamente a escrita de SQL repetitivo. Recursos como paginação automática, controle de concorrência otimista (optimistic locking) e geradores de código tornam a camada de acesso a dados altamente produtiva e menos propensa a erros.
Validação e Testes do Sistema
Objetivos dos Testes
A fase de testes é crítica para garantir que o software atenda aos requisitos funcionais e não funcionais. O foco principal é identificar anomalias antes da implantação em produção, simulando cenários reais de uso. Através de testes de caixa preta, validamos a integridade das regras de negócio, a usabilidade da interface e a resiliência do sistema contra entradas inválidas.
Testes Funcionais
Os módulos centrais, como autenticação e gerenciamento de usuários, foram submetidos a testes rigorosos. Abaixo estão os cenários avaliados para o fluxo de login:
| Cenário de Entrada | Comportamento Esperado | Resultado Obtido | Status |
|---|---|---|---|
| Credenciais válidas e CAPTCHA correto | Acesso concedido ao painel | Login efetuado com sucesso | Aprovado |
| Senha incorreta com usuário válido | Bloqueio e mensagem de erro | Alerta de credenciais inválidas exibido | Aprovado |
| CAPTCHA inválido | Rejeição da requisição | Mensagem de erro de verificação | Aprovado |
| Campos obrigatórios em branco | Validação de formulário no cliente | Alertas de campos obrigatórios | Aprovado |
Para o módulo de administração de contas, os testes focaram na manipulação de registros:
| Ação Executada | Comportamento Esperado | Resultado Obtido | Status |
|---|---|---|---|
| Cadastro com dados únicos e válidos | Criação do registro e atualização da listagem | Novo usuário visível na tabela | Aprovado |
| Tentativa de cadastro com identificador duplicado | Interrupção e aviso de duplicidade | Erro de constraint única retornado | Aprovado |
| Exclusão de registro existente | Prompt de confirmação e remoção lógica/física | Registro removido após confirmação | Aprovado |
Conclusão dos Testes
Os ciclos de validação demonstraram que a arquitetura implementada suporta adequadamente as operações diárias de um ambiente acadêmico. As regras de validação de frontend e backend operam em sincronia, prevenindo inconsistências de dados e garantindo uma experiência fluida para administradores e alunos.
Implementação de Autenticação e Controle de Acesso
O mecanismo de segurança baseia-se em tokens stateless. Abaixo está a implementação refactorizada do controlador de acesso e do serviço de geração de credenciais.
@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationController {
private final UserProfileService profileService;
private final SessionTokenService tokenService;
public AuthenticationController(UserProfileService profileService, SessionTokenService tokenService) {
this.profileService = profileService;
this.tokenService = tokenService;
}
@SkipAuthorization
@PostMapping("/signin")
public ResponseEntity<ApiResponse> authenticate(@RequestBody LoginRequest credentials, HttpServletRequest httpRequest) {
Optional<UserProfile> profileOpt = profileService.findByIdentifier(credentials.getIdentifier());
if (profileOpt.isEmpty() || !profileOpt.get().verifySecret(credentials.getSecretKey())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.failure("Credenciais inválidas ou inexistentes."));
}
UserProfile user = profileOpt.get();
String jwtToken = tokenService.issueSessionToken(
user.getId(),
user.getIdentifier(),
user.getAccessLevel()
);
return ResponseEntity.ok(ApiResponse.success(Map.of("accessToken", jwtToken)));
}
}
@Service
public class SessionTokenService {
private final TokenRepository tokenRepository;
public SessionTokenService(TokenRepository tokenRepository) {
this.tokenRepository = tokenRepository;
}
public String issueSessionToken(Long userId, String identifier, String accessLevel) {
String rawToken = UUID.randomUUID().toString().replace("-", "") + UUID.randomUUID().toString().replace("-", "");
LocalDateTime expiration = LocalDateTime.now().plusHours(2);
Optional<TokenRecord> existingToken = tokenRepository.findByUserIdAndAccessLevel(userId, accessLevel);
if (existingToken.isPresent()) {
TokenRecord record = existingToken.get();
record.setAccessToken(rawToken);
record.setExpiresAt(expiration);
tokenRepository.save(record);
} else {
TokenRecord newRecord = new TokenRecord(userId, identifier, accessLevel, rawToken, expiration);
tokenRepository.save(newRecord);
}
return rawToken;
}
}
Para proteger as rotas, um interceptor analisa o cabeçalho HTTP e valida a sessão ativa:
@Component
public class SecurityInterceptor implements HandlerInterceptor {
private static final String AUTH_HEADER = "Authorization";
private final SessionTokenService tokenService;
public SecurityInterceptor(SessionTokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
configureCorsHeaders(request, response);
if (request.getMethod().equalsIgnoreCase(RequestMethod.OPTIONS.name())) {
response.setStatus(HttpStatus.OK.value());
return false;
}
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
if (method.hasMethodAnnotation(SkipAuthorization.class)) {
return true;
}
}
String bearerToken = request.getHeader(AUTH_HEADER);
if (bearerToken != null && !bearerToken.isBlank()) {
Optional<TokenRecord> session = tokenService.validateAndFetch(bearerToken);
if (session.isPresent()) {
request.setAttribute("currentUserId", session.get().getUserId());
request.setAttribute("currentRole", session.get().getAccessLevel());
return true;
}
}
rejectRequest(response, "Sessão expirada ou não autenticada.");
return false;
}
private void configureCorsHeaders(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", origin != null ? origin : "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", AUTH_HEADER + ", Content-Type");
response.setHeader("Access-Control-Allow-Credentials", "true");
}
private void rejectRequest(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"status\":\"error\", \"message\":\"" + message + "\"}");
}
}
Modelagem do Banco de Dados
A persistência das sessões ativas é gerenciada pela seguinte estrutura relacional:
CREATE TABLE `auth_sessions` (
`session_id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`user_identifier` varchar(150) NOT NULL,
`access_level` varchar(50) DEFAULT NULL,
`access_token` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expires_at` timestamp NOT NULL DEFAULT '1970-01-01 00:00:00',
PRIMARY KEY (`session_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de sessões ativas';
INSERT INTO `auth_sessions` (`user_id`, `user_identifier`, `access_level`, `access_token`, `expires_at`) VALUES
(101, 'admin_master', 'ADMIN', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', '2024-12-01 10:00:00'),
(205, 'student_joao', 'STUDENT', 'z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4', '2024-11-25 14:30:00'),
(302, 'prof_maria', 'TEACHER', 'q1w2e3r4t5y6u7i8o9p0a1s2d3f4g5h6', '2024-11-28 09:15:00');