MTITEK.com
Spring Framework / Spring Security

Spring Security

This project covers Spring Security in two contexts: a form-login MVC app (mtitek-spring-security-mvc) and a stateless HTTP Basic REST API (mtitek-spring-security-rest). Both use an in-memory user store with BCrypt-encoded passwords and a custom UserDetails implementation. Security is configured exclusively via a SecurityFilterChain bean.

Maven Dependencies

<!-- both projects -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- MVC project test scope only -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc-test</artifactId>
    <scope>test</scope>
</dependency>

UserDetails — AppUser

@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@RequiredArgsConstructor
public class AppUser implements UserDetails {
    private final String username;
    private final String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }
}

Security Configuration — MVC

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login").permitAll()
            .anyRequest().hasRole("USER")
        )
        .formLogin(form -> form
            .loginPage("/login")
            .defaultSuccessUrl("/", true)
            .permitAll()
        )
        .logout(logout -> logout
            .logoutSuccessUrl("/login")
            .permitAll()
        )
        .build();
}

Security Configuration — REST

@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    return http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/appProfiles/**").hasRole("USER")
            .anyRequest().permitAll()
        )
        .httpBasic(Customizer.withDefaults())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .csrf(csrf -> csrf.disable())
        .build();
}

MVC Login Template

// LoginController handles GET only — POST /login is intercepted by Spring Security
@Controller
@RequestMapping("/login")
public class LoginController {
    @GetMapping
    public String loginForm() {
        return "login";
    }
}

Testing — @WebMvcTest with Spring Security

@WebMvcTest(HomeController.class)
public class HomeControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHomePage() throws Exception {
        mockMvc.perform(get("/")).andExpect(status().is4xxClientError());
    }
}