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