Skip to content

Spring Security

Yash edited this page Apr 8, 2025 · 2 revisions

image

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Setting up Spring Security in a separate module is a great way to modularize your application and separate concerns. Here's a full guide on how to do that, including dependencies, configuration, and properties setup.

🔧 Project Structure Example

my-project/
│
├── security-module/         <-- Spring Security config here
│   ├── build.gradle / pom.xml
│   └── src/main/java/...
│
├── main-application/
│   ├── build.gradle / pom.xml
│   └── src/main/java/...
│
└── settings.gradle / parent pom.xml

1️⃣ security-module Setup Maven:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2️⃣ Create Security Configuration

Spring Security REST APIs - HTTP Basic Authentication

🔐 What is httpBasic?

httpBasic is a type of authentication where the client sends the username and password in every HTTP request, encoded in the Authorization header.

Example:

Authorization: Basic base64(username:password)

Spring Security handles decoding and checking these credentials behind the scenes.

✅ How to Implement HTTP Basic Authentication - Enable httpBasic in your security config

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic() // <--- Enables Basic Auth
            .and()
            .csrf().disable(); // Disable CSRF for testing APIs easily (not recommended for prod)

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
            User.withUsername("admin")
                .password("{noop}admin123") // {noop} disables password encoding
                .roles("ADMIN")
                .build()
        );
    }
}

🔒 Notes on Password Encoding

  • {noop} means no encoding, used for testing.
  • In real apps, use BCrypt:
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Then update the user password like:

.password(passwordEncoder().encode("admin123"))


🔐 Spring Security REST APIs - HTTP Basic/FormLogin Authentication

✔️ HttpBasicSecurityConfig ✔️ FormLoginSecurityConfig
@Configuration
@EnableWebSecurity
public class HttpBasicSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((authorizeRequests) ->
authorizeRequests
.antMatchers("/login", "/public/").permitAll() // ⬅️ Allow login without auth, exclude context-path=/myworld
.antMatchers("/slf4j/").permitAll()  // ⬅️ Allow direct access
//.antMatchers("/myworld/slf4j/**").permitAll()  // exclude context-path to Allow direct access
.anyRequest().authenticated()          // Everything else requires authentication
);
http.httpBasic();
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
@Configuration
@EnableWebSecurity
public class FormLoginSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests( (authorizeRequests) -> authorizeRequests
.antMatchers("/login", "/logout", "/public/").permitAll()
.antMatchers("/slf4j/").permitAll()  // Allow direct access exclude context-path=/myworld
.anyRequest().authenticated()          // Everything else requires authentication
);
http.formLogin() // ✅ this alone enables default login page
  //🔥 Important: DO NOT set .loginPage(...) unless you serve an HTML custom login page yourself.
  .loginProcessingUrl("/login") // exclude context-path=/myworld
    .successHandler((request, response, authentication) -> {
    response.setStatus(HttpServletResponse.SC_OK);
    response.setContentType("application/json");
    response.getWriter().write("{\"message\": \"Login successful\"}");
  })
  .failureHandler((request, response, exception) -> {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.setContentType("application/json");
    response.getWriter().write("{\"error\": \"Login failed\"}");
  });

http.logout()
  .logoutUrl("/logout") // exclude context-path=/myworld
  .logoutSuccessHandler((request, response, authentication) -> {
    response.setStatus(HttpServletResponse.SC_OK);
    response.setContentType("application/json");
    response.getWriter().write("{\"message\": \"Logout successful\"}");
  });
http.csrf().disable(); // For REST APIs, CSRF can be disabled

return http.build();

}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = ...;
return new InMemoryUserDetailsManager(user);
}
}
}
Standard user agents (such as Internet Explorer and Netscape)
✉️ Spring Console Log
logging.level.org.springframework.security=DEBUG

SecurityContextImpl UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=1841D51C998DA0D704E926B8C31C3EB6] Granted Authorities=[ROLE_ADMIN]]

 o.s.s.web.DefaultSecurityFilterChain : Will secure any request with 
[ org.springframework.security.web.authentication
.www.BasicAuthenticationFilter extends OncePerRequestFilter ]

http://localhost:8080/myworld/sample/json/responseEntity-string


o.s.security.web.FilterChainProxy : Securing GET /sample/json/responseEntity-string s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext o.s.s.w.s.HttpSessionRequestCache : Loaded matching saved request http://localhost:8080/myworld/sample/json/responseEntity-string o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/myworld/sample/json/responseEntity-string to session s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest] s.w.a.DelegatingAuthenticationEntryPoint : No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint@73b57096 w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request o.s.security.web.FilterChainProxy : Securing GET /error s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext o.s.security.web.FilterChainProxy : Secured GET /error w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request o.s.security.web.FilterChainProxy : Securing GET /sample/json/responseEntity-string s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext o.s.s.a.dao.DaoAuthenticationProvider : Authenticated user o.s.s.w.a.www.BasicAuthenticationFilter : Set SecurityContextHolder to UsernamePasswordAuthenticationToken [#####] o.s.s.w.s.HttpSessionRequestCache : Loaded matching saved request http://localhost:8080/myworld/sample/json/responseEntity-string .s.ChangeSessionIdAuthenticationStrategy : Changed session id from 1841D51C998DA0D704E926B8C31C3EB6 w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [#####]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@7056d126] o.s.security.web.FilterChainProxy : Secured GET /sample/json/responseEntity-string w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [#####]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@7056d126] w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [#####]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@7056d126] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

o.s.s.web.DefaultSecurityFilterChain : Will secure any request with 
[ org.springframework.security.web.authentication
.UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter, 
.ui.DefaultLoginPageGeneratingFilter  extends GenericFilterBean, 
.ui.DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter
]

Go to: http://localhost:8080/myworld/sample/json/responseEntity-string → Status Code: 🔁 302 Found (Redirecting to /login) If not logged in, Spring will redirect you to: http://localhost:8080/myworld/login ...which shows the default login page. http://localhost:8080/myworld/login?error → Bad credentials, Status Code: ✅ 200 OK After login, it redirects to the original URL unless you override that with a custom success handler. http://localhost:8080/myworld/sample/json/responseEntity-string → Status Code: ✅ 200 OK


o.s.security.web.FilterChainProxy : Securing GET /sample/json/responseEntity-string s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext o.s.s.w.session.SessionManagementFilter : Request requested invalid session id 9E9C15D56CFC768778CA821188999DDA o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/myworld/sample/json/responseEntity-string to session o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/myworld/login

o.s.security.web.FilterChainProxy : Securing POST /login s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext o.s.s.a.dao.DaoAuthenticationProvider : Authenticated user w.a.UsernamePasswordAuthenticationFilter : Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=*] w.c.HttpSessionSecurityContextRepository : Created HttpSession as SecurityContext is non-default w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal= #####]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@5251cc00] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

o.s.security.web.FilterChainProxy : Securing GET /sample/json/responseEntity-string w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=#####]] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=#####]] o.s.security.web.FilterChainProxy : Secured GET /sample/json/responseEntity-string s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

🔁 Your Request/Response Breakdown Headers
Request Headers
  Authorization: "Basic YWRtaW46YWRtaW4xMjM="
  Cookie: JSESSIONID={{someID}}
Response Headers
   Set-Cookie: JSESSIONID=446F0C4728FA4C230BFDE6EDE7DD1F9E; Path=/myworld; HttpOnly
Request Headers
  Content-Type: multipart/form-data; boundary=--------------------------952892955974462193381526
Response Headers
  Set-Cookie: JSESSIONID=7A3AC066FE7BF6F5A026282D793C1B93; Path=/myworld; HttpOnly
  Location: http://localhost:8080/myworld/

HTTP Basic Authentication model RFC 1945, Section 11.1 RFC 2617, Section 2

The "basic" authentication scheme is based on the model that the client must authenticate itself with a user-ID and a password for each realm.

In the request Headers, the Authorization header passes the API a Base64 encoded string representing your username and password values, appended to the text Basic as follows:

Basic <Base64 encoded username and password>

✉️ Spring log: org.springframework.security.web.authentication.www.BasicAuthenticationFilter extends OncePerRequestFilter,

Processes a HTTP request's BASIC authorization headers, putting the result into the SecurityContextHolder.

In summary, this filter is responsible for processing any request that has a HTTPrequest header of Authorization with an authentication scheme of Basic and a Base64-encoded username:password token. Forexample, to authenticate user "Aladdin" with password "open sesame" the followingheader would be presented:

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

This filter can be used to provide BASIC authentication services to both remotingprotocol clients (such as Hessian and SOAP) as well as standard user agents (such asInternet Explorer and Netscape).

If authentication is successful, the resulting Authentication object will beplaced into the SecurityContextHolder.

If authentication fails and ignoreFailure is false (thedefault), an AuthenticationEntryPoint implementation is called (unless the ignoreFailure property is set to true). Usually this should be BasicAuthenticationEntryPoint, which will prompt the user to authenticate againvia BASIC authentication.

Basic authentication is an attractive protocol because it is simple and widelydeployed. However, it still transmits a password in clear text and as such isundesirable in many situations.

Note that if a RememberMeServices is set, this filter will automatically sendback remember-me details to the client. Therefore, subsequent requests will not need topresent a BASIC authentication header as they will be authenticated using theremember-me mechanism.

✅ How Spring Security Auth Flow Works (Default)

Step Server check - HTTP status
Request with session cookie Spring checks session validity
Valid session from same client ✅ 200 OK
Session is expired or restarted ❌ 401, even with JSESSIONID
Request without session, with Basic header Spring authenticates user and starts new session
✅ Authenticated
Request without session and without Basic header Spring returns ❌ 401 Unauthorized

🔁 Your Request/Response Breakdown Headers on GET http://localhost:8080/myworld/sample/json/string

Even though HTTP Basic Auth is stateless, Spring Security by default uses HTTP sessions behind the scenes unless configured as stateless.

🔽 First Request:

👎 No Header, New Client: (HTTP status : # → HTTP/1.1 401 Unauthorized)
Request Headers with no Authorization

👍 With Basic Header: (HTTP status : # → HTTP/1.1 200 OK)
Request Headers
  Authorization: "Basic YWRtaW46YWRtaW4xMjM="      → This is a Base64 encoded string of username:password. Decoded: admin:admin123
  User-Agent: PostmanRuntime/7.37.3                → You can clear Cookie's from postman Manually - Cookies tab (top-right near the request URL bar).
  # Cookie: JSESSIONID=399E414B4CF6FFFC9194DB0EEE  → You're reusing a previously issued session ID — probably from a previous successful login.


Even though HTTP Basic Auth is stateless, Spring Security by default uses HTTP sessions behind the scenes unless configured as stateless.
 * Spring Security sees your credentials (via Basic Auth) ✅
 * It validates them and then starts a new session 🆕
 * It returns a new JSESSIONID cookie to the client.   → Server creates a session and returns Set-Cookie: JSESSIONID=...

Response Headers
   Set-Cookie: JSESSIONID=446F0C4728FA4C230BFDE6EDE7DD1F9E; Path=/myworld; HttpOnly

🔽 From same client second/next Request on-words, client may send only the cookie (session reuse), or include both cookie + Basic header

Request Headers
    Cookie: JSESSIONID=446F0C4728FA4C230BFDE6EDE7DD1F9E

HTTP Basic Auth is stateless
  * Server uses session to skip re-authentication unless expired

Response Headers

🔐 Why Different Clients Get 401 Unauthorized (Even if Session Exists)

  • JSESSIONID is not valid for the new client:
    • HTTP sessions are server-side and usually tied to the client (via cookies).
    • When you send the same session ID from a different client, Spring Security doesn't associate that session with a valid login.
  • Session doesn’t exist or has expired:
    • If you're using a short timeout (e.g., 1 min), it may have expired already.
    • Or, if the server restarted, all sessions were wiped.
  • Spring Security checks session first, then checks for credentials:
    • If no valid session is found, and no Authorization header is present,
    • Spring responds with:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Realm"

🔐 What is a "Realm" in Basic Authentication? A realm is just a string label used by the server to identify the protected area or scope that requires authentication.

💬 Example: If a server responds with this header:

WWW-Authenticate: Basic realm="Admin Area"

Clone this wiki locally