Plataforma de Compartilhamento de Notas com SpringBoot, Vue e uniapp: Arquitetura e Implementação

Visão Geral da Arquitetura

O projeto consiste em uma aplicação web para registro e compartilhamento de notas, construída com uma arquitetura de separação entre frontend e backend. O lado do servidor utiliza Spring Boot como framework principal, enquanto a interface do usuário é desenvolvida com Vue.js e a aplicação mobile utiliza uniapp.

Backend - Spring Boot

O Spring Boot oferece servidores embutidos como Tomcat, Jetty e Undertow, eliminando a necessidade de configurações manuais adicionais. Sua funcionalidade de autoconfiguração detecta as dependências do projeto e ajusta o comportamento da aplicação automaticamente. Frameworks complementares como Spring Data, Spring Security e Spring Cloud estão disponíveis para acelerar o desenvolvimento e facilitar a integração com outras tecnologias.

Frontend - Vue.js

Vue.js utiliza a tecnologia de Virtual DOM, que mantém uma estrutura de dados em memória para otimziar as manipulações do DOM. Com binding reativo de dados, qualquer alteração nos dados reflete automaticamente na interface, permitindo que o desenvolvedor foque na lógica de dados em vez de atualizações manuais de UI. A abordagem baseada em componentes promove reutilização e facilita a manutenção.

Persistência - MyBatis-Plus

MyBatis-Plus é uma ferramenta de aprimoramento do MyBatis, compatível com MySQL, Oracle, SQL Server e PostgreSQL. Oferece uma API rica e anotações que reduzem drasticamente a necessidade de escrita manual de SQL. Inclui recursos como paginação automática, consultas dinâmicas, bloqueio otimista e um gerador de código que cria entidades, interfaces Mapper e arquivos XML de mapeamento.

Estratégia de Testes do Sistema

O processo de validação visa identificar falhas sob diferentes perspectivas e cenários, garantindo que todos os módulos funcionem conforme as especificações. Os testes são conduzidos com foco na experiência do usuário, simulando situações reais de uso.

Teste de Autenticação

A validação do login verifica credenciais, captcha e permissões de papel. A tabela abaixo ilustra os cenários testados:

Entrada Resultado Esperado Resultado Obtido Análise
Usuário: admin / Senha: 123456 / Captcha: válido Acesso concedido Login bem-sucedido Conforme esperado
Usuário: admin / Senha: 111111 / Captcha: válido Senha inválida Mensagem de senha incorreta Conforme esperado
Usuário: admin / Senha: 123456 / Captcha: inválido Captcha inválido Mensagem de captcha incorreto Conforme esperado
Usuário: vazio / Senha: 123456 / Captcha: válido Campo obrigatório Mensagem de usuário obrigatório Conforme esperado

Teste de Gestão de Usuários

A gestão de usuários engloba criação, edição, remoção e busca. Os testes verificam validações de campos obrigatórios, duplicidade de nomes e confirmação de exclusão:

Entrada Resultado Esperado Resultado Obtido Análise
Dados válidos de novo usuário Caadstro realizado Usuário listado Conforme esperado
Alteração de dados existentes Atualização concluída Dados modificados na lista Conforme esperado
Exclusão de registro Confirmação e remoção Registro removido Conforme esperado
Cadastro sem nome Bloqueio por campo vazio Mensagem de validação Conforme esperado
Nome já cadastrado Rejeição por duplicidade Mensagem de nome em uso Conforme esperado

Implementação do Código

Endpoint de Autenticação

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private UsuarioServico usuarioServico;

    @Autowired
    private SessaoServico sessaoServico;

    @SkipAuth
    @PostMapping("/entrar")
    public ResponseEntity<Map<String, Object>> autenticar(@RequestBody CredenciaisDTO credenciais,
                                                          HttpServletRequest requisicao) {
        Optional<Usuario> usuarioOpt = usuarioServico
            .buscarPorLogin(credenciais.getLogin());

        if (usuarioOpt.isEmpty() ||
            !usuarioOpt.get().getSenha().equals(credenciais.getSenha())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("mensagem", "Credenciais inválidas"));
        }

        Usuario usuario = usuarioOpt.get();
        String chaveAcesso = sessaoServico.criarSessao(
            usuario.getId(),
            usuario.getLogin(),
            usuario.getPapel()
        );

        return ResponseEntity.ok(Map.of(
            "accessToken", chaveAcesso,
            "papel", usuario.getPapel()
        ));
    }
}

Geração e Gerenciamento de Token

@Service
public class SessaoServicoImpl implements SessaoServico {

    @Autowired
    private TokenRepositorio tokenRepositorio;

    @Override
    public String criarSessao(Long usuarioId, String login, String papel) {
        Optional<TokenEntidade> existente = tokenRepositorio
            .findByUsuarioIdAndPapel(usuarioId, papel);

        String novaChave = UUID.randomUUID().toString().replace("-", "");
        LocalDateTime expiracao = LocalDateTime.now().plusHours(1);

        if (existente.isPresent()) {
            TokenEntidade token = existente.get();
            token.setChaveAcesso(novaChave);
            token.setDataExpiracao(expiracao);
            tokenRepositorio.save(token);
        } else {
            TokenEntidade novoToken = new TokenEntidade();
            novoToken.setUsuarioId(usuarioId);
            novoToken.setLogin(login);
            novoToken.setPapel(papel);
            novoToken.setChaveAcesso(novaChave);
            novoToken.setDataExpiracao(expiracao);
            tokenRepositorio.save(novoToken);
        }

        return novaChave;
    }

    @Override
    public TokenEntidade validarChave(String chave) {
        return tokenRepositorio.findByChaveAcesso(chave)
            .filter(t -> t.getDataExpiracao().isAfter(LocalDateTime.now()))
            .orElse(null);
    }
}

Interceptador de Autorização

@Component
public class AutenticacaoInterceptador implements HandlerInterceptor {

    private static final String HEADER_AUTORIZACAO = "Authorization";

    @Autowired
    private SessaoServico sessaoServico;

    @Override
    public boolean preHandle(HttpServletRequest req,
                             HttpServletResponse resp,
                             Object handler) throws Exception {

        configurarHeadersCors(resp, req);

        if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
            resp.setStatus(HttpServletResponse.SC_OK);
            return false;
        }

        if (!(handler instanceof HandlerMethod metodo)) {
            return true;
        }

        SkipAuth anotacao = metodo.getMethodAnnotation(SkipAuth.class);
        if (anotacao != null) {
            return true;
        }

        String headerToken = req.getHeader(HEADER_AUTORIZACAO);
        TokenEntidade sessao = null;

        if (StringUtils.isNotBlank(headerToken)) {
            sessao = sessaoServico.validarChave(headerToken);
        }

        if (sessao != null) {
            req.getSession().setAttribute("usuarioId", sessao.getUsuarioId());
            req.getSession().setAttribute("papel", sessao.getPapel());
            req.getSession().setAttribute("login", sessao.getLogin());
            return true;
        }

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json; charset=utf-8");
        resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        try (PrintWriter escritor = resp.getWriter()) {
            escritor.write("{\"codigo\": 401, \"mensagem\": \"Autenticação necessária\"}");
        }

        return false;
    }

    private void configurarHeadersCors(HttpServletResponse resp, HttpServletRequest req) {
        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, request-source, Authorization, Origin, Content-Type, Cookie, Accept");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
    }
}

Estrutura do Banco de Dados

-- Estrutura da tabela de tokens de sessão
DROP TABLE IF EXISTS `tb_sessao_token`;
CREATE TABLE `tb_sessao_token` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'Identificador único',
  `usuario_id` BIGINT(20) NOT NULL COMMENT 'Identificador do usuário',
  `login` VARCHAR(100) NOT NULL COMMENT 'Nome de login',
  `tabela_origem` VARCHAR(100) DEFAULT NULL COMMENT 'Tabela de origem do usuário',
  `papel` VARCHAR(100) DEFAULT NULL COMMENT 'Papel do usuário no sistema',
  `chave_acesso` VARCHAR(200) NOT NULL COMMENT 'Token de acesso',
  `data_criacao` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Data de criação',
  `data_expiracao` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Data de expiração',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_usuario_papel` (`usuario_id`, `papel`),
  INDEX `idx_chave` (`chave_acesso`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='Tabela de controle de sessões';

-- Registros de exemplo
INSERT INTO `tb_sessao_token` (`usuario_id`, `login`, `tabela_origem`, `papel`, `chave_acesso`, `data_expiracao`)
VALUES (1, 'admin', 'tb_usuarios', 'ADMIN', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', '2024-03-20 18:00:00');

INSERT INTO `tb_sessao_token` (`usuario_id`, `login`, `tabela_origem`, `papel`, `chave_acesso`, `data_expiracao`)
VALUES (15, 'joao_silva', 'tb_estudantes', 'ESTUDANTE', 'q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2', '2024-03-18 14:30:00');

INSERT INTO `tb_sessao_token` (`usuario_id`, `login`, `tabela_origem`, `papel`, `chave_acesso`, `data_expiracao`)
VALUES (22, 'maria_costa', 'tb_moderadores', 'MODERADOR', 'g3h4i5j6k7l8m9n0o1p2q3r4s5t6u7v8', '2024-03-19 09:15:00');

Tags: SpringBoot Vue.js uniapp MyBatis-Plus Autenticação JWT

Publicado em 8-7 02:57