Security Implementation
Iquea Commerce implements a stateless JWT-based authentication system with role-based access control (RBAC) using Spring Security.
Security Architecture
Security Configuration
SecurityConfig.java
Configures Spring Security with JWT authentication and endpoint authorization.
package com.edu.mcs.Iquea.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Disable CSRF for stateless JWT
.csrf(csrf -> csrf.disable())
// Stateless session management
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Authorization rules
.authorizeHttpRequests(auth -> auth
// Public routes
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/productos/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/categorias/**").permitAll()
// ADMIN-only routes
.requestMatchers(HttpMethod.POST, "/api/productos/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/productos/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/productos/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/categorias/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/categorias/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/categorias/**").hasRole("ADMIN")
// Everything else requires authentication
.anyRequest().authenticated()
)
// Add JWT filter before Spring Security's authentication filter
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(
Arrays.asList("http://localhost:5173", "http://127.0.0.1:5173")
);
configuration.setAllowedMethods(
Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
);
configuration.setAllowedHeaders(
Arrays.asList("Authorization", "Content-Type", "X-Requested-With",
"Accept", "Origin", "Access-Control-Request-Method",
"Access-Control-Request-Headers")
);
configuration.setExposedHeaders(Arrays.asList("Authorization"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Key configurations:
- CSRF disabled - Not needed for stateless JWT authentication
- Stateless sessions - No server-side session storage
- Role-based access - ADMIN vs CLIENTE permissions
- CORS enabled - Allows frontend access from localhost:5173
JWT Implementation
JwtUtil.java
Utility class for generating and validating JWT tokens.
package com.edu.mcs.Iquea.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
public class JwtUtil {
private static final String SECRET =
"iquea-super-secret-key-2024-must-be-long-enough-32chars";
private static final long EXPIRATION_MS = 86400000L; // 24 hours
private final SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes());
/**
* Generate JWT token with email and role claims
*/
public String generarToken(String email, String rol) {
return Jwts.builder()
.subject(email)
.claim("rol", rol)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(key)
.compact();
}
/**
* Extract email from token
*/
public String extraerEmail(String token) {
return parsear(token).getPayload().getSubject();
}
/**
* Extract role from token
*/
public String extraerRol(String token) {
return parsear(token).getPayload().get("rol", String.class);
}
/**
* Validate token signature and expiration
*/
public boolean esValido(String token) {
try {
parsear(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
private Jws<Claims> parsear(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token);
}
}
Token structure:
Key features:
- HMAC-SHA256 signature algorithm
- 24-hour expiration - Tokens expire after 1 day
- Custom claims - Stores user role in token
- Automatic validation - Checks signature and expiration
In production, the secret key should be stored in environment variables or a secrets manager, not hardcoded.
JwtFilter.java
Intercepts requests to validate JWT tokens and set authentication context.
package com.edu.mcs.Iquea.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// Extract Authorization header
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7); // Remove "Bearer " prefix
if (jwtUtil.esValido(token)) {
String email = jwtUtil.extraerEmail(token);
String rol = jwtUtil.extraerRol(token);
// Create authentication object
var auth = new UsernamePasswordAuthenticationToken(
email,
null,
List.of(new SimpleGrantedAuthority("ROLE_" + rol))
);
// Set authentication in Spring Security context
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
// Continue filter chain
filterChain.doFilter(request, response);
}
}
Filter workflow:
- Extract
Authorization header from request
- Remove
Bearer prefix to get token
- Validate token signature and expiration
- Extract email and role from token claims
- Create Spring Security authentication object
- Add
ROLE_ prefix (Spring Security convention)
- Set authentication in security context
- Continue with request processing
The ROLE_ prefix is automatically added by the filter, so hasRole("ADMIN") checks for ROLE_ADMIN authority.
Authentication Flow
AuthController.java
Handles user registration and login.
package com.edu.mcs.Iquea.controllers;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import com.edu.mcs.Iquea.models.Usuario;
import com.edu.mcs.Iquea.models.dto.detalle.LoginDTO;
import com.edu.mcs.Iquea.models.dto.detalle.TokenDTO;
import com.edu.mcs.Iquea.models.dto.detalle.UsuarioDetalleDTO;
import com.edu.mcs.Iquea.repositories.UsuarioRepository;
import com.edu.mcs.Iquea.security.JwtUtil;
import com.edu.mcs.Iquea.services.IUsuarioService;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final IUsuarioService usuarioService;
private final UsuarioRepository usuarioRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;
public AuthController(IUsuarioService usuarioService,
UsuarioRepository usuarioRepository,
JwtUtil jwtUtil,
PasswordEncoder passwordEncoder) {
this.usuarioService = usuarioService;
this.usuarioRepository = usuarioRepository;
this.jwtUtil = jwtUtil;
this.passwordEncoder = passwordEncoder;
}
/**
* POST /api/auth/registro
* Register new user and return JWT token
*/
@PostMapping("/registro")
public ResponseEntity<TokenDTO> registro(@RequestBody UsuarioDetalleDTO dto) {
Usuario creado = usuarioService.crearUsuario(dto);
String token = jwtUtil.generarToken(
creado.getEmail().getValue(),
creado.getRol().name()
);
return ResponseEntity.ok(new TokenDTO(token));
}
/**
* POST /api/auth/login
* Authenticate user and return JWT token
*/
@PostMapping("/login")
public ResponseEntity<TokenDTO> login(@RequestBody LoginDTO dto) {
Usuario usuario = usuarioRepository.findByEmailValue(dto.getEmail())
.orElseThrow(() -> new RuntimeException("Usuario no encontrado"));
if (!passwordEncoder.matches(dto.getPassword(), usuario.getPassword())) {
throw new IllegalArgumentException("Contraseña incorrecta");
}
String token = jwtUtil.generarToken(
usuario.getEmail().getValue(),
usuario.getRol().name()
);
return ResponseEntity.ok(new TokenDTO(token));
}
}
Registration Flow
Login Flow
Role-Based Access Control
User Roles
public enum RolUsuario {
ADMIN, // Full access to products, categories, users
CLIENTE // Can browse products and place orders
}
Endpoint Authorization Matrix
| Endpoint | Method | Public | CLIENTE | ADMIN |
|---|
/api/auth/registro | POST | ✅ | ✅ | ✅ |
/api/auth/login | POST | ✅ | ✅ | ✅ |
/api/productos | GET | ✅ | ✅ | ✅ |
/api/productos | POST | ❌ | ❌ | ✅ |
/api/productos/{id} | PUT | ❌ | ❌ | ✅ |
/api/productos/{id} | DELETE | ❌ | ❌ | ✅ |
/api/categorias | GET | ✅ | ✅ | ✅ |
/api/categorias | POST | ❌ | ❌ | ✅ |
/api/pedidos | GET | ❌ | ✅ | ✅ |
/api/pedidos | POST | ❌ | ✅ | ✅ |
/api/usuarios | GET | ❌ | ❌ | ✅ |
Making Authenticated Requests
Registration:
curl -X POST http://localhost:8080/api/auth/registro \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "secret123",
"username": "johndoe",
"nombre": "John",
"apellidos": "Doe",
"fecha_nacimiento": "1990-01-01",
"direccion_envio": "123 Main St"
}'
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Using the token:
curl -X GET http://localhost:8080/api/pedidos \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Password Encryption
BCrypt configuration:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Hashing passwords:
String plainPassword = "secret123";
String hashedPassword = passwordEncoder.encode(plainPassword);
// Result: $2a$10$N9qo8uLOickgx2ZMRZoMye...
Verifying passwords:
boolean matches = passwordEncoder.matches(
dto.getPassword(), // Plain text from request
usuario.getPassword() // Hashed from database
);
BCrypt automatically generates salts and is resistant to rainbow table attacks.
Security Best Practices
Stateless Authentication
JWT tokens contain all necessary information, no server-side session storage needed
Password Hashing
BCrypt with automatic salting protects user passwords
Token Expiration
24-hour expiration limits exposure from stolen tokens
CORS Configuration
Restricts API access to authorized frontend origins
Security Checklist
- ✅ Passwords hashed with BCrypt
- ✅ JWT tokens expire after 24 hours
- ✅ CSRF protection disabled (stateless JWT)
- ✅ Role-based access control (ADMIN/CLIENTE)
- ✅ Public endpoints documented
- ✅ CORS configured for frontend
- ⚠️ Move secret key to environment variables
- ⚠️ Add refresh token mechanism
- ⚠️ Implement token blacklist for logout
- ⚠️ Add rate limiting for login attempts