Arquitetura e Implementação de Sistema de Gestão de Campus Inteligente com Spring Boot, Vue e UniApp

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');

Tags: SpringBoot Vue.js MyBatis-Plus uniapp java

Publicado em 6-24 02:00