I have a Spring Boot Thymeleaf project that I'm trying to introduce a custom login page in following https://www.baeldung.com/spring-security-login. When I browse to the application root it redirects to http://localhost:8080/login.html but instead of the login form I get the error:
The page isn’t redirecting properly. Firefox has detected that the server is redirecting the request for this address in a way that will never complete.
Before adding the login customization I had a generic login popup which worked. It's something in the additions I made with customizing login. My WebSecurityConfig:
@EnableWebSecurity
@Configuration
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.authorizeRequests()
.antMatchers("/login*", "/", "/search", "/browse", "/recipes/**", "/tags/**", "/edit/**", "/delete/**")
.hasRole("ADMIN")
.antMatchers("/login*", "/", "/search", "/browse", "/recipes/**", "/tags/**")
.hasRole("USER")
.anyRequest()
.authenticated()
.and()
.formLogin().loginPage("/login.html")
.defaultSuccessUrl("/", true)
.failureUrl("/login.html?error=true");
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withUsername("foote").password(passwordEncoder().encode("userpassword")).roles("USER")
.build();
UserDetails admin = User.withUsername("admin").password(passwordEncoder().encode("adminpassword"))
.roles("USER", "ADMIN").build();
return new InMemoryUserDetailsManager(user, admin);
}
}
My login.html (as per the article):
<html>
<head></head>
<body>
<h1>Login</h1>
<form name='f' action="login" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
</form>
</body>
</html>
The application log with "logging.level.org.springframework.security=DEBUG" in application.properties:
2022-11-19T10:07:06.116 [http-nio-8080-exec-1] [DEBUG] [o.s.security.web.FilterChainProxy] Securing GET /
2022-11-19T10:07:06.119 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.c.SecurityContextPersistenceFilter] Set SecurityContextHolder to empty SecurityContext
2022-11-19T10:07:06.121 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.a.AnonymousAuthenticationFilter] Set SecurityContextHolder to anonymous SecurityContext
2022-11-19T10:07:06.127 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.a.i.FilterSecurityInterceptor] Failed to authorize filter invocation [GET /] with attributes [hasRole('ROLE_USER')]
2022-11-19T10:07:06.135 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.s.HttpSessionRequestCache] Saved request http://localhost:8080/ to session
2022-11-19T10:07:06.135 [http-nio-8080-exec-1] [DEBUG] [o.s.s.web.DefaultRedirectStrategy] Redirecting to http://localhost:8080/login.html
2022-11-19T10:07:06.136 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.c.HttpSessionSecurityContextRepository] Did not store empty SecurityContext
2022-11-19T10:07:06.137 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.c.HttpSessionSecurityContextRepository] Did not store empty SecurityContext
2022-11-19T10:07:06.137 [http-nio-8080-exec-1] [DEBUG] [o.s.s.w.c.SecurityContextPersistenceFilter] Cleared SecurityContextHolder to complete request
2022-11-19T10:07:06.169 [http-nio-8080-exec-2] [DEBUG] [o.s.security.web.FilterChainProxy] Securing GET /login.html
2022-11-19T10:07:06.169 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.c.SecurityContextPersistenceFilter] Set SecurityContextHolder to empty SecurityContext
2022-11-19T10:07:06.170 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.a.AnonymousAuthenticationFilter] Set SecurityContextHolder to anonymous SecurityContext
2022-11-19T10:07:06.170 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.a.i.FilterSecurityInterceptor] Failed to authorize filter invocation [GET /login.html] with attributes [hasRole('ROLE_USER')]
2022-11-19T10:07:06.171 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.s.HttpSessionRequestCache] Saved request http://localhost:8080/login.html to session
2022-11-19T10:07:06.171 [http-nio-8080-exec-2] [DEBUG] [o.s.s.web.DefaultRedirectStrategy] Redirecting to http://localhost:8080/login.html
2022-11-19T10:07:06.171 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.c.HttpSessionSecurityContextRepository] Did not store empty SecurityContext
2022-11-19T10:07:06.171 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.c.HttpSessionSecurityContextRepository] Did not store empty SecurityContext
2022-11-19T10:07:06.171 [http-nio-8080-exec-2] [DEBUG] [o.s.s.w.c.SecurityContextPersistenceFilter] Cleared SecurityContextHolder to complete request
2022-11-19T10:07:06.189 [http-nio-8080-exec-3] [DEBUG] [o.s.security.web.FilterChainProxy] Securing GET /login.html
2022-11-19T10:07:06.190 [http-nio-8080-exec-3] [DEBUG] [o.s.s.w.c.SecurityContextPersistenceFilter] Set SecurityContextHolder to empty SecurityContext
...
The last action, securing /login.html, repeats a dozen times before the error, cyclic I think.
What am I missing or doing wrong?
UPDATE: Not getting any response; is there more info I can add?
CodePudding user response:
So I think I've got this working. Turns out there was a few things missing.
In WebSecurityConfig I said use a custom login page but I didn't provide access to it. So I added permitAll() after declaring loginPage. And I provided access to required resources prior to login (first antMatcher). I was also missing some patterns in my roles' antMatchers. The final result:
@EnableWebSecurity
@Configuration
public class WebSecurityConfig {
/**
* Configures HTTP security.
*
* @param http {@link HttpSecurity}
* @return {@link SecurityFilterChain}
* @throws Exception when the internet falls over.
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.authorizeRequests()
.antMatchers("/login*", "/resources/**", "/error")
.permitAll()
.antMatchers("/", "/login*", "/error", "/search", "/browse", "/recipes/**", "/tags/**",
"/resources/**", "/add", "/create", "/uploadImage", "/edit/**", "/delete/**")
.hasRole("ADMIN")
.antMatchers("/", "/login*", "/error", "/search", "/browse", "/recipes/**", "/tags/**",
"/resources/**")
.hasRole("USER")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/", true)
.failureUrl("/login?error=true");
return http.build();
}
/**
* Use password encoding.
*
* @return {@link PasswordEncoder}
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* Username and password and roles.
*
* @return {@link InMemoryUserDetailsManager}
*/
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withUsername("user").password(passwordEncoder().encode("password")).roles("USER")
.build();
UserDetails admin = User.withUsername("admin").password(passwordEncoder().encode("password"))
.roles("USER", "ADMIN").build();
return new InMemoryUserDetailsManager(user, admin);
}
/**
* Specify resources to allow without credentials.
*
* @return {@link WebSecurityCustomizer}
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedPercent(true);
return web -> web.httpFirewall(firewall)
.ignoring()
.antMatchers("/styles/**", "/js/**", "/images/**", "/fonts/**", "/resources/**");
}
}