Real World Example

The ShopEase SecurityConfig

ShopEase has two kinds of clients: the admin web panel (server-rendered, browser, sessions) and the REST API (used by scripts and, later, the React store). Like many real companies, it uses two chains:

package com.shopease.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    @Order(1)
    SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET, "/api/products/**", "/api/categories/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/auth/register").permitAll()
                .anyRequest().authenticated())
            .httpBasic(basic -> basic.realmName("ShopEase API"))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable());
        return http.build();
    }

    @Bean
    @Order(2)
    SecurityFilterChain webChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/", "/login", "/actuator/health").permitAll()
                .anyRequest().authenticated())
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/admin/dashboard", false)
                .permitAll())
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
                .deleteCookies("JSESSIONID"));
        return http.build();
    }
}

Testing Every Rule

Example

A quick smoke test the team runs after every security change:

curl -s -o /dev/null -w "%{http_code}  GET  /api/products\n" localhost:8080/api/products
curl -s -o /dev/null -w "%{http_code}  POST /api/orders (anonymous)\n" -X POST localhost:8080/api/orders
curl -s -o /dev/null -w "%{http_code}  POST /api/orders (asha)\n" -u asha:asha123 -X POST localhost:8080/api/orders -H "Content-Type: application/json" -d '{"items":[{"productId":1,"quantity":1}]}'
curl -s -o /dev/null -w "%{http_code}  GET  /admin/dashboard (browser)\n" -H "Accept: text/html" localhost:8080/admin/dashboard
curl -s -o /dev/null -w "%{http_code}  GET  /actuator/health\n" localhost:8080/actuator/health

Example output

200  GET  /api/products
401  POST /api/orders (anonymous)
201  POST /api/orders (asha)
302  GET  /admin/dashboard (browser)
200  GET  /actuator/health

Why Each Choice

  • Two chains: API clients get clean 401s; browsers get a login page and redirects.
  • STATELESS + CSRF off for /api/**: no cookies, so no CSRF risk and no memory used for sessions.
  • CSRF on for the web chain: the admin panel uses a session cookie.
  • anyRequest().authenticated() last in both chains: new endpoints are private by default.
  • Admin-only rules (hasRole("ADMIN")) and JWT for the React store come in the medium level.