implementado el soft-delete

This commit is contained in:
Jaime Jiménez
2025-09-29 15:35:41 +02:00
parent 865b1573b9
commit 656bb5bad2
11 changed files with 522 additions and 241 deletions

View File

@ -1,10 +1,16 @@
package com.imprimelibros.erp.config;
import java.util.Locale;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -15,11 +21,19 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import com.imprimelibros.erp.users.User;
import com.imprimelibros.erp.users.UserDao;
import com.imprimelibros.erp.users.UserDetailsImpl;
import jakarta.servlet.http.HttpServletRequest;
@Configuration
public class SecurityConfig {
@ -32,12 +46,16 @@ public class SecurityConfig {
// ========== Beans base ==========
@Bean
public UserDetailsService userDetailsService(UserDao repo) {
return username -> {
User u = repo.findByUserNameAndEnabledTrue(username);
if (u == null) throw new UsernameNotFoundException("No existe: " + username);
return new UserDetailsImpl(u);
};
public UserDetailsService userDetailsService(UserDao repo, MessageSource messages) {
return username -> repo
.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username) // <-- @EntityGraph aplicado
.map(UserDetailsImpl::new) // dentro del ctor precalculamos authorities
.orElseThrow(() -> {
var locale = org.springframework.context.i18n.LocaleContextHolder.getLocale();
String msg = messages.getMessage("usuarios.error.no-activo", null,
"Usuario no activo o no existe", locale);
return new org.springframework.security.core.userdetails.UsernameNotFoundException(msg);
});
}
@Bean
@ -45,84 +63,131 @@ public class SecurityConfig {
return new BCryptPasswordEncoder();
}
// Remember-me (tabla persistent_logins)
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la
// tabla
return repo;
}
// Provider que soporta UsernamePasswordAuthenticationToken
@Bean
public AuthenticationProvider daoAuthenticationProvider(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider p = new DaoAuthenticationProvider();
p.setUserDetailsService(userDetailsService);
p.setPasswordEncoder(passwordEncoder);
return p;
// ✅ constructor recomendado (sin deprecations)
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder); // este setter NO está deprecado
return provider;
}
// Remember-me (tabla persistent_logins)
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la tabla
return repo;
// Provider que soporta UsernamePasswordAuthenticationToken
private static RequestMatcher pathStartsWith(String... prefixes) {
return new RequestMatcher() {
@Override
public boolean matches(HttpServletRequest request) {
String uri = request.getRequestURI();
if (uri == null)
return false;
for (String p : prefixes) {
if (uri.startsWith(p))
return true;
}
return false;
}
};
}
// ========== Filtro de seguridad ==========
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
@Value("${security.rememberme.key}") String keyRememberMe,
UserDetailsService userDetailsService,
PersistentTokenRepository tokenRepo,
AuthenticationProvider daoAuthenticationProvider
) throws Exception {
AuthenticationProvider daoAuthenticationProvider) throws Exception {
http
// Registra explícitamente el provider para Username/Password
.authenticationProvider(daoAuthenticationProvider)
.authenticationProvider(daoAuthenticationProvider)
.sessionManagement(session -> session
.invalidSessionUrl("/login?expired")
.maximumSessions(1)
)
.sessionManagement(session -> session
.invalidSessionUrl("/login?expired")
.maximumSessions(1))
.csrf(csrf -> csrf
.ignoringRequestMatchers("/presupuesto/public/**")
)
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
.csrf(csrf -> csrf
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login",
"/assets/**", "/css/**", "/js/**", "/images/**",
"/public/**", "/presupuesto/public/**",
"/error", "/favicon.ico").permitAll()
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
.anyRequest().authenticated()
)
// ====== RequestCache: sólo navegaciones HTML reales ======
.requestCache(rc -> {
HttpSessionRequestCache cache = new HttpSessionRequestCache();
.formLogin(login -> login
.loginPage("/login").permitAll()
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/", false)
.failureUrl("/login?error") // útil para diagnosticar
)
// Navegación HTML (por tipo o por cabecera Accept)
RequestMatcher htmlPage = new OrRequestMatcher(
new MediaTypeRequestMatcher(MediaType.TEXT_HTML),
new MediaTypeRequestMatcher(MediaType.APPLICATION_XHTML_XML),
new RequestHeaderRequestMatcher("Accept", "text/html"));
.rememberMe(rm -> rm
.key(keyRememberMe)
.rememberMeParameter("remember-me")
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
.tokenValiditySeconds(60 * 60 * 24 * 2)
.userDetailsService(userDetailsService)
.tokenRepository(tokenRepo)
)
// No AJAX
RequestMatcher nonAjax = new NegatedRequestMatcher(
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
.permitAll()
);
// Excluir sondas .well-known
RequestMatcher notWellKnown = new NegatedRequestMatcher(pathStartsWith("/.well-known/"));
// Excluir estáticos: comunes + tu /assets/**
RequestMatcher notStatic = new AndRequestMatcher(
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
new NegatedRequestMatcher(pathStartsWith("/assets/")));
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown));
rc.requestCache(cache);
})
// ========================================================
.authorizeHttpRequests(auth -> auth
// Aquí usa patrones String (no deprecados)
.requestMatchers(
"/",
"/login",
"/assets/**",
"/css/**",
"/js/**",
"/images/**",
"/public/**",
"/presupuesto/public/**",
"/error",
"/favicon.ico",
"/.well-known/**" // opcional
).permitAll()
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
.anyRequest().authenticated())
.formLogin(login -> login
.loginPage("/login").permitAll()
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/", false) // respeta SavedRequest (ya filtrada)
.failureUrl("/login?error"))
.rememberMe(rm -> rm
.key(keyRememberMe)
.rememberMeParameter("remember-me")
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
.tokenValiditySeconds(60 * 60 * 24 * 2)
.userDetailsService(userDetailsService)
.tokenRepository(tokenRepo))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
.permitAll());
return http.build();
}