se añadio jwt en el backend
Some checks failed
Deploy Spring Boot App / build-and-deploy (push) Has been cancelled
Some checks failed
Deploy Spring Boot App / build-and-deploy (push) Has been cancelled
This commit is contained in:
167
FRONTEND_JWT_MIGRATION.txt
Normal file
167
FRONTEND_JWT_MIGRATION.txt
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
================================================================================
|
||||||
|
MIGRACIÓN REACT NATIVE: AUTENTICACIÓN CON JWT (backend Fercogan)
|
||||||
|
================================================================================
|
||||||
|
Documento para adaptar el frontend (React Native) a los nuevos endpoints y al
|
||||||
|
uso del token Bearer. Base URL de ejemplo: https://testapp.digitaltelecom.net
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
1. RESUMEN DEL CAMBIO
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
- Antes: el "login" usaba GET /api/usuarios/confirmado/{email} sin contraseña y
|
||||||
|
guardaba solo email en AsyncStorage (inseguro).
|
||||||
|
- Ahora: el login real es POST /api/usuarios/login con email (username) y
|
||||||
|
contraseña. El servidor devuelve un JWT que debe enviarse en las peticiones
|
||||||
|
protegidas: header Authorization: Bearer <token>.
|
||||||
|
- La mayoría de rutas /api/* (remates, lotes, pujas, etc.) requieren JWT salvo
|
||||||
|
las excepciones listadas abajo.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
2. ENDPOINTS PÚBLICOS (sin Authorization)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
POST /api/usuarios/login → login (body JSON)
|
||||||
|
POST /api/usuarios/registrar → registro
|
||||||
|
GET /api/usuarios/confirmado/{username} → solo verificación registro/aprobado
|
||||||
|
(útil en flujo signup; no sustituye login)
|
||||||
|
GET /auth/verificar/{username} → mismo concepto que confirmado (signup)
|
||||||
|
GET /auth/existe/{username} → según implementación actual
|
||||||
|
OPTIONS /** → CORS preflight
|
||||||
|
/ws/** , /contador/** → según necesidad (actualmente públicos)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
3. LOGIN (SIGN IN) — REEMPLAZA confirmado + AsyncStorage sin token
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Método: POST
|
||||||
|
URL: {BASE_URL}/api/usuarios/login
|
||||||
|
Headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Body (JSON):
|
||||||
|
{
|
||||||
|
"username": "<email del usuario>",
|
||||||
|
"password": "<contraseña en texto plano; va por HTTPS>"
|
||||||
|
}
|
||||||
|
|
||||||
|
Respuesta 200 (JSON) — guardar estos valores en AsyncStorage:
|
||||||
|
{
|
||||||
|
"token": "<JWT>",
|
||||||
|
"tokenType": "Bearer",
|
||||||
|
"expiresInMs": 86400000,
|
||||||
|
"username": "...",
|
||||||
|
"userId": 1,
|
||||||
|
"rolId": 1,
|
||||||
|
"authenticated": true,
|
||||||
|
"confirmed": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Errores:
|
||||||
|
- 401 + { "error": "Credenciales inválidas" } → usuario inexistente o contraseña incorrecta
|
||||||
|
- 403 + { "error": "Usuario pendiente de aprobación" } → existe pero aún no aprobado
|
||||||
|
|
||||||
|
Acciones en el cliente tras 200:
|
||||||
|
1) AsyncStorage.setItem("authToken", data.token);
|
||||||
|
2) AsyncStorage.setItem("usuario", data.username);
|
||||||
|
3) AsyncStorage.setItem("rol", String(data.rolId)); // ya no hace falta GET /rol/{email}
|
||||||
|
4) AsyncStorage.setItem("isLoggedIn", "true");
|
||||||
|
5) Opcional: guardar userId para depuración.
|
||||||
|
|
||||||
|
NO guardar la contraseña en AsyncStorage.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
4. SESIÓN ACTUAL / USUARIO LOGUEADO
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
GET {BASE_URL}/api/usuarios/me
|
||||||
|
Headers:
|
||||||
|
Authorization: Bearer <authToken>
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
Respuesta 200 ejemplo:
|
||||||
|
{
|
||||||
|
"username": "...",
|
||||||
|
"userId": 1,
|
||||||
|
"rolId": 1,
|
||||||
|
"confirmed": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Usar al abrir la app para validar que el token sigue siendo válido; si 401,
|
||||||
|
limpiar AsyncStorage y volver a pantalla de login.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
5. PETICIONES PROTEGIDAS (remates, pujas, lotes, cabañas, etc.)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
En TODAS las llamadas fetch/axios a /api/... (excepto login, registrar,
|
||||||
|
confirmado y auth/verificar según corresponda), añadir:
|
||||||
|
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + (await AsyncStorage.getItem("authToken"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Si el servidor responde 401, el token expiró o es inválido: cerrar sesión en
|
||||||
|
cliente (borrar authToken, isLoggedIn) y redirigir a login.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
6. SIGN UP (REGISTRO) — CAMBIOS MÍNIMOS
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
- GET /auth/verificar/{email} puede seguir usándose sin token para decidir si
|
||||||
|
el usuario ya existe o está aprobado (flujo actual).
|
||||||
|
- POST /api/usuarios/registrar sigue siendo público; body igual que antes
|
||||||
|
(username=email, password, celular, ci, nombre, rol).
|
||||||
|
|
||||||
|
Tras registro exitoso, el usuario sigue pendiente de aprobación: NO recibirá
|
||||||
|
JWT válido para app hasta que un admin apruebe; el login con POST /login
|
||||||
|
devolverá 403 mientras tanto.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
7. EJEMPLO handleSignIn (lógica sugerida, pseudocódigo)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
async function handleSignIn() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
if (!validateEmail(email)) { setError("Email inválido"); setLoading(false); return; }
|
||||||
|
|
||||||
|
const res = await fetch(BASE + "/api/usuarios/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username: email, password: password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
setError(data.error || "Credenciales inválidas");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === 403) {
|
||||||
|
setError(data.error || "Cuenta pendiente de aprobación");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
setError("Error de servidor");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AsyncStorage.setItem("authToken", data.token);
|
||||||
|
await AsyncStorage.setItem("usuario", data.username);
|
||||||
|
await AsyncStorage.setItem("rol", String(data.rolId));
|
||||||
|
await AsyncStorage.setItem("isLoggedIn", "true");
|
||||||
|
navigation.replace("RematesList");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Eliminar: GET confirmado/{email} como sustituto del login, y la llamada
|
||||||
|
separada a GET /api/usuarios/rol/{email} tras login (los datos vienen en la
|
||||||
|
respuesta del login).
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
8. VARIABLES DE ENTORNO BACKEND (operaciones)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
En el servidor, definir JWT_SECRET (cadena larga y aleatoria, mínimo 32 bytes
|
||||||
|
en UTF-8) y opcionalmente JWT_EXPIRATION_MS. El valor por defecto en
|
||||||
|
application.properties es solo para desarrollo.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
FIN DEL DOCUMENTO
|
||||||
|
================================================================================
|
||||||
17
pom.xml
17
pom.xml
@@ -66,6 +66,23 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class CorsConfig {
|
|||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**") // permite todos los endpoints
|
registry.addMapping("/**") // permite todos los endpoints
|
||||||
.allowedOrigins("*") // permite todas las apps cliente
|
.allowedOrigins("*") // permite todas las apps cliente
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // métodos permitidos
|
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") // métodos permitidos
|
||||||
.allowedHeaders("*"); // cabeceras permitidas
|
.allowedHeaders("*"); // cabeceras permitidas
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
package com.example.fercoganbackend.configuration;
|
package com.example.fercoganbackend.configuration;
|
||||||
|
|
||||||
import com.example.fercoganbackend.service.UsuarioDetailsService;
|
import com.example.fercoganbackend.security.JwtAuthenticationFilter;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.config.Customizer;
|
|
||||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final UsuarioDetailsService usuarioDetailsService;
|
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
public SecurityConfig(UsuarioDetailsService usuarioDetailsService) {
|
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||||
this.usuarioDetailsService = usuarioDetailsService;
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -39,20 +39,21 @@ public class SecurityConfig {
|
|||||||
http
|
http
|
||||||
.securityMatcher("/**")
|
.securityMatcher("/**")
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/ws/**").permitAll() // WebSocket
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
|
.requestMatchers("/ws/**").permitAll()
|
||||||
.requestMatchers("/auth/**").permitAll()
|
.requestMatchers("/auth/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/usuarios/login").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/usuarios/registrar").permitAll()
|
||||||
.requestMatchers("/api/usuarios/confirmado/**").permitAll()
|
.requestMatchers("/api/usuarios/confirmado/**").permitAll()
|
||||||
.requestMatchers("/favicon.ico", "/error", "/static/**", "/contador/**", "/api/**").permitAll()
|
.requestMatchers("/favicon.ico", "/error", "/static/**", "/contador/**").permitAll()
|
||||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
.requestMatchers("/admin/**").hasAuthority("ADMIN")
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.httpBasic(Customizer.withDefaults());
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
package com.example.fercoganbackend.controller;
|
package com.example.fercoganbackend.controller;
|
||||||
|
|
||||||
|
import com.example.fercoganbackend.dto.LoginRequest;
|
||||||
|
import com.example.fercoganbackend.dto.LoginResponse;
|
||||||
import com.example.fercoganbackend.entity.Rol;
|
import com.example.fercoganbackend.entity.Rol;
|
||||||
import com.example.fercoganbackend.entity.Roles;
|
import com.example.fercoganbackend.entity.Roles;
|
||||||
import com.example.fercoganbackend.entity.Usuario;
|
import com.example.fercoganbackend.entity.Usuario;
|
||||||
import com.example.fercoganbackend.otros.ConfirmadoTF;
|
import com.example.fercoganbackend.otros.ConfirmadoTF;
|
||||||
|
import com.example.fercoganbackend.security.JwtService;
|
||||||
import com.example.fercoganbackend.service.UsuarioService;
|
import com.example.fercoganbackend.service.UsuarioService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.DisabledException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -24,6 +30,51 @@ public class UserController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UsuarioService service;
|
private UsuarioService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtService jwtService;
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
|
||||||
|
try {
|
||||||
|
authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||||
|
);
|
||||||
|
Usuario usuario = service.findByUsername(request.getUsername());
|
||||||
|
String token = jwtService.generateToken(usuario);
|
||||||
|
|
||||||
|
LoginResponse body = new LoginResponse();
|
||||||
|
body.setToken(token);
|
||||||
|
body.setTokenType("Bearer");
|
||||||
|
body.setExpiresInMs(jwtService.getExpirationMs());
|
||||||
|
body.setUsername(usuario.getUsername());
|
||||||
|
body.setUserId(usuario.getId());
|
||||||
|
body.setRolId(usuario.getRol() != null ? usuario.getRol().getId() : null);
|
||||||
|
body.setAuthenticated(true);
|
||||||
|
body.setConfirmed(usuario.isAprobado());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
} catch (BadCredentialsException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(Map.of("error", "Credenciales inválidas"));
|
||||||
|
} catch (DisabledException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(Map.of("error", "Usuario pendiente de aprobación"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
public ResponseEntity<Map<String, Object>> me(Authentication authentication) {
|
||||||
|
Usuario u = service.findByUsername(authentication.getName());
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("username", u.getUsername());
|
||||||
|
m.put("userId", u.getId());
|
||||||
|
m.put("rolId", u.getRol() != null ? u.getRol().getId() : null);
|
||||||
|
m.put("confirmed", u.isAprobado());
|
||||||
|
return ResponseEntity.ok(m);
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Obtener todos los usuarios
|
// ✅ Obtener todos los usuarios
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.fercoganbackend.dto;
|
||||||
|
|
||||||
|
public class LoginRequest {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.example.fercoganbackend.dto;
|
||||||
|
|
||||||
|
public class LoginResponse {
|
||||||
|
private String token;
|
||||||
|
private String tokenType = "Bearer";
|
||||||
|
private long expiresInMs;
|
||||||
|
private String username;
|
||||||
|
private Long userId;
|
||||||
|
private Long rolId;
|
||||||
|
private boolean authenticated;
|
||||||
|
private boolean confirmed;
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenType() {
|
||||||
|
return tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTokenType(String tokenType) {
|
||||||
|
this.tokenType = tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpiresInMs() {
|
||||||
|
return expiresInMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpiresInMs(long expiresInMs) {
|
||||||
|
this.expiresInMs = expiresInMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getRolId() {
|
||||||
|
return rolId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRolId(Long rolId) {
|
||||||
|
this.rolId = rolId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthenticated() {
|
||||||
|
return authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticated(boolean authenticated) {
|
||||||
|
this.authenticated = authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfirmed() {
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmed(boolean confirmed) {
|
||||||
|
this.confirmed = confirmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.example.fercoganbackend.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtService jwtService;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
@NonNull HttpServletRequest request,
|
||||||
|
@NonNull HttpServletResponse response,
|
||||||
|
@NonNull FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
final String authHeader = request.getHeader("Authorization");
|
||||||
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String jwt = authHeader.substring(7);
|
||||||
|
try {
|
||||||
|
final String username = jwtService.extractUsername(jwt);
|
||||||
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
if (jwtService.isTokenValid(jwt, userDetails)) {
|
||||||
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||||
|
userDetails,
|
||||||
|
null,
|
||||||
|
userDetails.getAuthorities()
|
||||||
|
);
|
||||||
|
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Token inválido o expirado: no se establece autenticación
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.example.fercoganbackend.security;
|
||||||
|
|
||||||
|
import com.example.fercoganbackend.entity.Usuario;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.io.Decoders;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class JwtService {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration-ms}")
|
||||||
|
private long expirationMs;
|
||||||
|
|
||||||
|
public long getExpirationMs() {
|
||||||
|
return expirationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String extractUsername(String token) {
|
||||||
|
return extractClaim(token, Claims::getSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||||
|
final Claims claims = extractAllClaims(token);
|
||||||
|
return claimsResolver.apply(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(Usuario usuario) {
|
||||||
|
Map<String, Object> extra = new HashMap<>();
|
||||||
|
extra.put("uid", usuario.getId());
|
||||||
|
if (usuario.getRol() != null) {
|
||||||
|
extra.put("rid", usuario.getRol().getId());
|
||||||
|
}
|
||||||
|
extra.put("confirmed", usuario.isAprobado());
|
||||||
|
return buildToken(extra, usuario.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildToken(Map<String, Object> extraClaims, String subject) {
|
||||||
|
Date now = new Date();
|
||||||
|
Date expiry = new Date(now.getTime() + expirationMs);
|
||||||
|
return Jwts.builder()
|
||||||
|
.claims(extraClaims)
|
||||||
|
.subject(subject)
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiration(expiry)
|
||||||
|
.signWith(getSigningKey())
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTokenValid(String token, UserDetails userDetails) {
|
||||||
|
final String username = extractUsername(token);
|
||||||
|
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTokenExpired(String token) {
|
||||||
|
return extractClaim(token, Claims::getExpiration).before(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claims extractAllClaims(String token) {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(getSigningKey())
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecretKey getSigningKey() {
|
||||||
|
byte[] bytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (bytes.length < 32) {
|
||||||
|
throw new IllegalStateException("jwt.secret debe tener al menos 32 bytes (UTF-8)");
|
||||||
|
}
|
||||||
|
return Keys.hmacShaKeyFor(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,11 @@ public class UsuarioService {
|
|||||||
.orElse(null); // O podrías lanzar una excepción si el usuario no existe
|
.orElse(null); // O podrías lanzar una excepción si el usuario no existe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Usuario findByUsername(String username) {
|
||||||
|
return repo.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Usuario no encontrado: " + username));
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional // Asegura la integridad de los datos
|
@Transactional // Asegura la integridad de los datos
|
||||||
public Usuario actualizarParcial(Long id, UserController.UsuarioRequest request) {
|
public Usuario actualizarParcial(Long id, UserController.UsuarioRequest request) {
|
||||||
// 1. Buscamos el usuario actual
|
// 1. Buscamos el usuario actual
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
|||||||
logging.level.org.springframework.security=DEBUG
|
logging.level.org.springframework.security=DEBUG
|
||||||
logging.level.org.hibernate.SQL=DEBUG
|
logging.level.org.hibernate.SQL=DEBUG
|
||||||
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
|
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
|
||||||
|
|
||||||
|
# JWT (cambiar jwt.secret en producción; mínimo 256 bits para HS256)
|
||||||
|
jwt.secret=${JWT_SECRET:change-this-in-production-use-long-random-string-at-least-32-chars}
|
||||||
|
jwt.expiration-ms=${JWT_EXPIRATION_MS:86400000}
|
||||||
|
|||||||
Reference in New Issue
Block a user