Arquitetura Base Minimalista para Sistemas Full-Stack com Vue.js e Spring Boot

Visão Geral da Stack Tecnológica

Esta estrutura foi desenvolvida para acelerar a criação de sistemas corporativos divididos em camadas distintas. O lado do cliente utiliza Vue.js integrado ao Element-UI e Avue, com tipagem estática via TypeScript. O servidor opera sobre Spring Boot, empregando MyBatis-Plus para persistência em MySQL e JSON Web Tokens (JWT) para controle de sessão stateless.

Organização do Cliente (Frontend)

Proteção de Rotas e Navegação

O mecanismo de roteamento implementa uma guarda global que verifica a presença de credenciais válidas antes de liberar o acesso às views protegidas. Caso a autenticação falhe, o usuário é redirecionado automaticamente para a tela de entrada.

router.beforeEach((target, origin, proceed) => {
  const pageTitle = target.meta.title as string;
  if (pageTitle) document.title = pageTitle;

  const hasCredentials = AuthManager.retrieveToken();
  const isPublicRoute = target.name === 'SignIn';

  if (!isPublicRoute && !hasCredentials) {
    return proceed({ name: 'SignIn' });
  }
  proceed();
});

Gerenciamento de Estado Global

O armazenamento centralizado gerencia o perfil do usuário autenticado. As mutações são acionadas exclusivamente por actions, garantindo fluxo unidirecional. Para evitar perda de dados durante recarregamentos, o estado é sincronizado com o armazenamento de sessão do navegador.

import Vue from 'vue';
import Vuex from 'vuex';
import { UserProfile } from '@/models/profile';
import { ApiClient } from '@/services/api';
import { StorageHandler } from '@/utils/storage';
import { AuthManager } from '@/utils/auth';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    currentUser: null as UserProfile | null,
  },
  getters: {
    activeUser: (ctx) => ctx.currentUser,
  },
  mutations: {
    UPDATE_PROFILE(ctx, payload: UserProfile) {
      ctx.currentUser = payload;
    },
  },
  actions: {
    async loadProfile({ commit }) {
      const endpoint = '/api/v1/users/me';
      const payload = { credential: AuthManager.retrieveToken() };
      const response = await ApiClient.post(endpoint, payload);
      const userData = response.data as UserProfile;
      commit('UPDATE_PROFILE', userData);
      StorageHandler.save('session_profile', userData);
    },
  },
});

Camada de Utilitários

A abstração de requisições HTTP configura timeouts e credenciais globais. Interceptors injetam o token no cabeçalho de saída e tratam respostas de erro, redirecionando para a tela de acesso quando a sessão expira. Módulos auxiliares gerenciam chaves de acesso, endpoints dinâmicos e cache volátil, isolando a lógica de negócio da comunicação de rede.

Organização do Servidor (Back end)

Configuração de Ambiente

O arquivo de propriedades define parâmetros de conexão, formatação de datas e chaves de criptografia. A URL do cliente é explicitamente declarada para políticas de origem cruzada.

server:
  port: 9090
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/app_db?useSSL=false&serverTimezone=UTC
    username: admin
    password: secure_pass
    driver-class-name: com.mysql.cj.jdbc.Driver
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: America/Sao_Paulo
client:
  origin: http://192.168.1.100:8080
security:
  jwt:
    key: my_super_secret_key_2024
    duration-ms: 43200000

Política de CORS

Uma classe de configuração expõe um bean que libera métodos e cabeçalhos específicos para o domínio do frontend, garantindo comunicação segura entre origens distintas.

@Configuration
public class CrossOriginSetup implements WebMvcConfigurer {
    @Value("${client.origin}")
    private String allowedOrigin;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins(allowedOrigin)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(7200);
    }
}

Serviço de Tokens JWT

A manipulação de credenciais stateless é centralizada em um componente responsável pela emissão, validação e extração de identificadores. A biblioteca java-jwt é utilizada para assinar e verificar a integridade dos payloads.

@Component
public class TokenService {
    private static long validityPeriod;
    private static String signingKey;

    @Value("${security.jwt.duration-ms}")
    public void setValidity(long ms) { validityPeriod = ms; }

    @Value("${security.jwt.key}")
    public void setKey(String key) { signingKey = key; }

    public String createToken(String subjectId) {
        Date expiration = new Date(System.currentTimeMillis() + validityPeriod);
        Algorithm algo = Algorithm.HMAC256(signingKey);
        return JWT.create()
                .withSubject(subjectId)
                .withIssuedAt(new Date())
                .withExpiresAt(expiration)
                .sign(algo);
    }

    public boolean isValid(String credential) {
        if (credential == null || credential.trim().isEmpty()) return false;
        try {
            Algorithm algo = Algorithm.HMAC256(signingKey);
            DecodedJWT decoded = JWT.require(algo).build().verify(credential);
            return !decoded.getExpiresAt().before(new Date());
        } catch (Exception ex) {
            return false;
        }
    }

    public String extractSubject(String credential) {
        try {
            Algorithm algo = Algorithm.HMAC256(signingKey);
            return JWT.require(algo).build().verify(credential).getSubject();
        } catch (Exception ex) {
            return null;
        }
    }
}

Filtro de Autenticação

Um filtro servlet intercepta requisições antes que alcancem os controladores. Endpoints públicos e requisições preflight são ignorados. Para as demais rotas, o cabeçalho de autorização é validado. Em caso de falha, uma resposta JSON é retornada com os cabeçalhos CORS necessários.

@Component
public class SecurityInterceptor implements Filter {
    @Value("${client.origin}")
    private String frontendUrl;

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String path = request.getRequestURI();
        String method = request.getMethod();

        boolean isLogin = path.endsWith("/auth/signin");
        boolean isPreflight = "OPTIONS".equalsIgnoreCase(method);

        if (!isLogin && !isPreflight) {
            String authHeader = request.getHeader("Authorization");
            if (!TokenService.isValid(authHeader)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=UTF-8");
                response.setHeader("Access-Control-Allow-Origin", frontendUrl);
                response.setHeader("Access-Control-Allow-Credentials", "true");
                response.getWriter().write("{\"error\":\"Sessão inválida ou expirada\"}");
                return;
            }
        }
        chain.doFilter(req, res);
    }
}

Controlaodr de Acesso

O endpoint REST gerencia o ciclo de vida da sessão. A atuenticação compara hashes MD5 das senhas armazenadas. O logout invalida o cookie no cliente, e a recuperação de perfil decodifica o token para retornar dados não sensíveis.

@RestController
@RequestMapping("/auth")
public class AccessController {
    @Autowired
    private UserAccountService accountService;

    @PostMapping("/signin")
    public ResponseEntity> authenticate(@RequestBody LoginRequest dto) {
        if (dto.getUsername() == null || dto.getPassword() == null) {
            return ResponseEntity.badRequest().body("Credenciais ausentes");
        }
        String hashedPass = DigestUtils.md5Hex(dto.getPassword());
        Optional<useraccount> account = accountService.findByCredentials(dto.getUsername(), hashedPass);

        if (account.isPresent()) {
            String token = TokenService.createToken(account.get().getId().toString());
            return ResponseEntity.ok(Collections.singletonMap("token", token));
        }
        return ResponseEntity.status(401).body("Falha na autenticação");
    }

    @PostMapping("/signout")
    public ResponseEntity> terminateSession(HttpServletResponse res) {
        Cookie cookie = new Cookie("APP_SESSION", "");
        cookie.setMaxAge(0);
        cookie.setPath("/");
        res.addCookie(cookie);
        return ResponseEntity.ok("Sessão encerrada");
    }

    @PostMapping("/profile")
    public ResponseEntity> fetchProfile(@RequestBody TokenRequest dto) {
        String uid = TokenService.extractSubject(dto.getToken());
        if (uid == null) return ResponseEntity.status(500).body("Token corrompido");

        UserAccount user = accountService.findById(uid);
        user.setPassword(null);
        return ResponseEntity.ok(user);
    }
}</useraccount>

Compilação e Implantação

Interface do Usuário

O script de build no package.json orquestra a geração dos ativos estáticos. O arquivo de configuração do Vue define o diretório de saída e o caminho base relativo.

"scripts": {
  "start": "vue-cli-service serve --mode development",
  "stage": "vue-cli-service build --mode staging",
  "release": "vue-cli-service build --mode production"
}
module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
  outputDir: 'release-build',
  assetsDir: 'assets',
};

A execução do comando de release gera uma pasta contendo os arquivos otimizados, prontos para serem servidos por Nginx, Apache ou qualquer servidor HTTP estático.

API Backend

O plugin do Maven no pom.xml empacota a aplicação em um JAR executável, definindo o diretório de destino.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <finalName>api-server</finalName>
        <outputDirectory>${project.basedir}/deploy</outputDirectory>
    </configuration>
</plugin>

O comando mvn clean package produz o artefato na pasta especificada. A inicialização é realizada via java -jar deploy/api-server.jar, disponibilizando os endpoints para consumo.

Tags: vuejs SpringBoot jwt TypeScript MyBatis-Plus

Publicado em 8-11 00:51