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.