Files
erp-imprimelibros/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java

169 lines
7.4 KiB
Java

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/")))
// ====== 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/")));
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown));
rc.requestCache(cache);
})
// ========================================================
.authorizeHttpRequests(auth -> auth
// Aquí usa patrones String (no deprecados)
.requestMatchers(
"/",
"/login",
"/signup",
"/verify",
"/reset-password",
"/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();
}
}