在持久登录应用程序中使用 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安全实现 UserDetailsService
// 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>
我有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安全实现 UserDetailsService
// 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>