package com.imprimelibros.erp.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.UserDetailsService; 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.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.UserServiceImpl; import jakarta.servlet.http.HttpServletRequest; @Configuration public class SecurityConfig { private final DataSource dataSource; public SecurityConfig(DataSource dataSource) { this.dataSource = dataSource; } // ========== Beans base ========== @Bean public PasswordEncoder passwordEncoder() { 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 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; } }; } @Bean public SecurityFilterChain securityFilterChain( HttpSecurity http, @Value("${security.rememberme.key}") String keyRememberMe, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepo, PasswordEncoder passwordEncoder, UserServiceImpl userServiceImpl) throws Exception { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userServiceImpl); provider.setPasswordEncoder(passwordEncoder); http.authenticationProvider(provider); http .authenticationProvider(provider) .sessionManagement(session -> session // .invalidSessionUrl("/login?expired") .maximumSessions(1)) // Ignora CSRF para tu recurso público (sin Ant/Mvc matchers) .csrf(csrf -> csrf .ignoringRequestMatchers(pathStartsWith("/presupuesto/public/"), pathStartsWith("/pagos/redsys/"))) // ====== RequestCache: sólo navegaciones HTML reales ====== .requestCache(rc -> { HttpSessionRequestCache cache = new HttpSessionRequestCache(); // 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")); // No AJAX RequestMatcher nonAjax = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); // 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/"))); RequestMatcher cartCount = new AndRequestMatcher( new NegatedRequestMatcher(PathRequest.toStaticResources() .atCommonLocations()), new NegatedRequestMatcher(pathStartsWith("/cart/count"))); cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown, cartCount)); rc.requestCache(cache); }) // ======================================================== .authorizeHttpRequests(auth -> auth // Aquí usa patrones String (no deprecados) .requestMatchers( "/", "/login", "/signup", "/verify", "/auth/password/**", "/assets/**", "/css/**", "/js/**", "/images/**", "/public/**", "/presupuesto/public/**", "/error", "/favicon.ico", "/.well-known/**", // opcional "/api/pdf/presupuesto/**", "/pagos/redsys/**" ) .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(); } }