在持久登录应用程序中使用 Spring Boot 2.6.X 和 JDK 1.11.XX 保护我的应用程序免受 CSRF 攻击

Protect my application from CSRF attacks with Spring Boot 2.6.X and JDK 1.11.XX in a persistent login application

我有4个问题和一个无法解决的问题。在代码中我添加了问号以突出问题。

我从 Spring Boot 2.0.X 和 Java 1.8.X 中的旧示例开始,在 Spring Boot 2 中开发了我的应用程序。我现在使用 Spring Boot 2.6.X 和 Java 1.11.XX(目前是 2.6.5 和 1.11.13)。在 html 页面中,我添加了这行代码:

    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

在java中我添加:

http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    //http.csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository());

我可以登录但无法注销。

使用 Spring Boot 2。0.X 我没有保护 WebApp,我使用了:

    http.csrf().disable();

1)为什么会这样?我该如何解决?

2) 是我用来让浏览器正确记住用户密码的方法,还是有必要通过 Spring Boot 2 以其他方式完成。 6.X?

3) 如果您对如何使我的应用程序现代化、使其更安全有任何其他建议,我很高兴收到您的来信。

谢谢

A) ConfigurazioneSpring安全扩展 WebSecurityConfigurerAdapter

    // 3) ?????????????
    // Configurazione di Spring Security.
    @Configuration
    @EnableWebSecurity
    public class ConfigurazioneSpringSecurity extends WebSecurityConfigurerAdapter {
    
        // Metodi per la gestione dell'utente autenticato.
        @Autowired
        GestioneUtentiSpringSecurity gestioneUtentiSpringSecurity;
        
        // Metodo per crittografare la password
        @Bean
        public static BCryptPasswordEncoder metodoCrittografia() {
            return new BCryptPasswordEncoder();
        }
        @Autowired
        public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
        }
    
        @Autowired
        private DataSource dataSource;
        // 2) ?????????????
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
            db.setDataSource(dataSource);
            return db;
        }
        // Se l'utente cambia la password il pulsante "Ricordami" continua a funzionare.
        @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        // Configurazione di Spring Security.
        @Override
        protected void configure(HttpSecurity http) throws Exception {
        // 1) ?????????????
        //http.csrf().disable();
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        //http.csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository());
            http.authorizeRequests().antMatchers(
                    "/",
                    "/login",
                    "/benvenuto",
                    "/registrazione",
                    "/registrazione-eseguita",
                    "/pagine-applicazione"
            ).permitAll();
            http.authorizeRequests().antMatchers("/area-riservata")
                    .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
            // ... ecc...
            http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/errore-403");
            http.authorizeRequests().and().formLogin()
                    .loginProcessingUrl("/pagina-login")
                    .loginPage("/login")
                    .defaultSuccessUrl("/")
                    .failureUrl("/login?errore=true")
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .and().logout().logoutUrl("/pagina-logout")
                    .logoutSuccessUrl("/login?logout=true");
    
            // 2) ?????????????
            http.authorizeRequests().and()
                    .rememberMe().tokenRepository(this.persistentTokenRepository())
                    .tokenValiditySeconds(365 * 24 * 60 * 60);
        }
    
    }

B) GestioneUtentiSpring安全实现 UserDetailsS​​ervice

    // 3) ?????????????
    // Creazione utente autenticato.
    @Service
    public class GestioneUtentiSpringSecurity implements UserDetailsService {
    
        @Autowired
        private UtenteRepository utenteRepository;
        @Autowired
        private RuoloRepository ruoloRepository;
        @Autowired
        //...
    
        // Creazione utente autenticato
        @Override
        public UserDetails loadUserByUsername(String nomeUtente) throws UsernameNotFoundException {
    
            // Si cerca l'utente nel database
            Utente utente = trovaUtenteConPrivilegiDiAutenticazione(nomeUtente);
            if (utente == null) {
                // System.out.println("L'utente " + nomeUtente + " non è stato trovato!");
                throw new UsernameNotFoundException("L'utente " + nomeUtente + " non è stato trovato nel database.");
            }
            // Si cercano i ruoli dell'utente
            List<String> ruoliUtente = null;
            try {
                ruoliUtente = this.ruoloRepository.trovaRuoliUtente(utente.getId());
            }catch (Exception b){
                ruoliUtente = null;
            }
    
            // Si caricano in una lista di oggetti GrantedAuthority i ruoli di un dato utente.
            // GrantedAuthority è una classe di Spring Security che contiene i privilegi di un utente.
            List<GrantedAuthority> grantList = null;
            try{
                grantList = new ArrayList<GrantedAuthority>();
                if (ruoliUtente != null) {
                    for (String ruolo : ruoliUtente) {
                        GrantedAuthority authority = new SimpleGrantedAuthority(ruolo);
                        grantList.add(authority);
                    }
                }
            }catch (Exception c){
                grantList = null;
            }
    
            // Si crea un oggetto specifico di Spring Security che rappresenta l'utente autenticato. Questo oggetto contiene 3
            // informazioni: nome utente, password e privilegi. Questi ultimi, in questa applicazione si fanno coincidere
            // con i ruoli.
            UserDetails userDetails = null;
            if((utente != null) && (ruoliUtente != null) && (grantList != null)){
                userDetails = (UserDetails) new User(utente.getNome(), utente.getPassword(), grantList);
            }
            return userDetails;
        }
    
        // Si cerca l'utente nel database
        public Utente trovaUtenteConPrivilegiDiAutenticazione(String nomeUtente){
            // ...
        }
    
    }

C) pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.6.5</version>
            <relativePath/> <!--/* cerca genitore dal repository */-->
        </parent>
        <groupId>...</groupId>
        <artifactId>...</artifactId>
        <version>...</version>
        <packaging>war</packaging>
        <name>...</name>
        <description>...</description>
        <properties>
            <java.version>11</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.thymeleaf.extras</groupId>
                <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            </dependency>
            <dependency>
                <groupId>org.postgresql</groupId>
                <artifactId>postgresql</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-csv</artifactId>
                <version>1.9.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
                <version>5.2.1</version>
            </dependency>
        </dependencies>
        <build>
            <finalName>gestioneutenti</finalName>
            <pluginManagement>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-war-plugin</artifactId>
                        <configuration>
                            <packagingExcludes>
                                WEB-INF/classes/it/...
                            </packagingExcludes>
                        </configuration>
                    </plugin>
                </plugins>
            </pluginManagement>
        </build>
    </project>

D) @Controller

    @Controller
    public class ControlloPagineWeb {

        @RequestMapping(value = "/login", method = RequestMethod.GET)
        public String paginaLogin(
                Model model,
                Principal principal,
                HttpSession session,
                HttpServletRequest request,
                HttpServletResponse response
        ) {
            variabiliGeneraliPerPaginaHTML(principal, model, session, request, response);
            model.addAttribute("titolo", "Login");
            model.addAttribute("messaggio", "Login");
            return "login";
        }

        @RequestMapping(value = "/logout-eseguito", method = RequestMethod.GET)
        public String logoutEseguitoConSuccesso(
                Model model,
                Principal principal,
                HttpSession session,
                HttpServletRequest request,
                HttpServletResponse response
        ) {
            variabiliGeneraliPerPaginaHTML(principal, model, session, request, response);
            model.addAttribute("titolo", "Login");
            model.addAttribute("messaggio", "Login");
            model.addAttribute("logoutEseguito", "Logout eseguito correttamente.");
            return "login";
        }

    }

E) 有时我会收到这个错误。

    D:\ALTRO\Java\jdk-11.0.13\bin\java.exe
    ...
    2022-03-29 21:23:09.245 ERROR 20124 --- [.1-8443-exec-10] org.apache.tomcat.util.net.NioEndpoint   : Error running socket processor
    java.lang.NullPointerException: null
    at java.base/sun.security.ssl.HKDF.extract(HKDF.java:93) ~[na:na]
    at java.base/sun.security.ssl.HKDF.extract(HKDF.java:119) ~[na:na]
    at java.base/sun.security.ssl.ServerHello.setUpPskKD(ServerHello.java:1169) ~[na:na]
    at java.base/sun.security.ssl.ServerHello$T13ServerHelloProducer.produce(ServerHello.java:547) ~[na:na]
    ...

F) 注销按钮:(/pagina-logout)

<li class="nav-item">
<a class="dropdown-item" th:if="${#request.userPrincipal != null}"
th:href="@{/pagina-logout}">
<b><i class="bi bi-box-arrow-right"></i> Logout</b>
</a>
</li>

G)注销按钮配置:(/pagina-logout)

        http.authorizeRequests().and().formLogin()
                .loginProcessingUrl("/pagina-login")
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?errore=true")
                .usernameParameter("username")
                .passwordParameter("password")
                .and().logout().logoutUrl("/pagina-logout")
                .logoutSuccessUrl("/login?logout=true");

如果启用了 CSRF 保护,那么通过 .logoutUrl(...) 配置的注销端点的请求必须是 POST。

最好的方法是将注销按钮更改为:

<li class="nav-item">
    <div class="dropdown-item" th:if="${#request.userPrincipal != null}">
        <form th:action="@{/pagina-logout}" method="post">
            <input id="logout" type="submit" value="Log Out"/>
        </form>
    </div>
</li>