Spring Security with Angular Frontend
Building the backend is half the battle. The Angular frontend must play its part too.
The VIP Lounge Wristband Analogy (Revisited):
When you pay at the Goa resort front desk, the receptionist hands you the wristband directly. They don’t mail it to your house. Similarly, when Angular sends a username/password to Spring Boot /login, Spring Boot responds with the JWT directly in the HTTP response body.
Angular takes this token and saves it. The next time Angular asks for data, it attaches the token to the request header: Authorization: Bearer <token>. This is the Bearer Token pattern.
- CORS (Cross-Origin Resource Sharing): Angular runs on
localhost:4200, Spring Boot onlocalhost:8080. The browser will block requests between them unless Spring Boot explicitly allows it inside the Security Filter. - Angular HTTP Interceptor: An Angular class that automatically intercepts every HTTP request and injects the
Authorization: Bearerheader. - Stateless Session: Spring Boot must not create
JSESSIONIDcookies. It must beSTATELESS.
Here is the complete, modern Spring Security 6 configuration combining JWT, CORS, and Statelessness.
package com.devinhyderabad;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. Enable CORS with default settings
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 2. Disable CSRF for stateless JWT APIs
.csrf(csrf -> csrf.disable())
// 3. URL Rules
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
// 4. Tell Spring NOT to create HTTP Sessions
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 5. Add our custom JWT filter before Spring's default password filter
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 6. Global CORS Configuration Bean
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:4200"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}How Angular Sends the Token:
// Angular Code (TypeScript)
import { HttpRequest, HttpHandlerFn } from '@angular/common/http';
export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
const token = localStorage.getItem('jwt_token');
if (token) {
const clonedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(clonedReq);
}
return next(req);
}SecurityConfig now includes:
- CORS:
corsConfigurationSource()bean explicitly allowslocalhost:4200. Without this, Angular gets a CORS error in the browser console. - SessionManagement(STATELESS): This is critical. Without this, Spring Security still creates a
JSESSIONIDcookie alongside the JWT, defeating the purpose of stateless auth. - addFilterBefore: Our
JwtAuthenticationFilterruns before Spring’s built-inUsernamePasswordAuthenticationFilter, ensuring the JWT is processed before any login check.
Angular Interceptor: The authInterceptor function is a functional interceptor (Angular 15+ style). It reads the JWT from localStorage and auto-injects it into every outgoing HTTP request. This saves you from manually adding headers to every API call in your Angular app.
Interview Question: “Why do we disable CSRF in a JWT-based Spring Boot application?”
Answer: CSRF (Cross-Site Request Forgery) protection is designed to protect against attacks where a malicious site forces the user’s browser to submit a state-changing request using the user’s existing session cookie. Since our REST API is stateless and uses Bearer tokens (stored in local storage or memory, not cookies), there is no session cookie to hijack. Therefore, CSRF protection is unnecessary and would only break API calls.
Enterprise Note: Storing JWTs in localStorage in Angular is easy but vulnerable to XSS (Cross-Site Scripting) attacks. The most secure enterprise pattern is to store the JWT in an HttpOnly cookie set by the Spring Boot backend, which protects it from JavaScript access. However, this requires configuring SameSite cookie attributes and adds complexity.
Key Takeaways
- ✅ CORS must be configured on the backend to allow Angular frontend requests
- ✅ SessionCreationPolicy.STATELESS prevents Spring from creating HTTP sessions
- ✅ JwtAuthenticationFilter is added before UsernamePasswordAuthenticationFilter
- ✅ Angular HttpInterceptor auto-injects the Bearer token into every request
- ✅ localStorage is simple but XSS-vulnerable; HttpOnly cookies are more secure
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login