Adding Spring Security to an Existing Spring Boot Project Without Breaking Everything
I added the Spring Security dependency to my existing Spring Boot project, and suddenly every endpoint returned 401 Unauthorized. My frontend couldn’t reach the API, my tests failed, and Swagger UI became inaccessible.
This is the classic “Spring Security breaks everything” moment that many developers face when retrofitting security into an existing application.
The Problem with Adding Security Later
When you add spring-boot-starter-security to an existing Spring Boot project, the framework’s auto-configuration kicks in immediately:
- All endpoints require authentication by default
- A generated password is created for the default user
- Your existing tests start failing
- Frontend integration breaks
- Development workflow is disrupted
Many developers on Reddit have faced this dilemma. One asked whether to add security at the beginning or after completing the project. The consensus was pragmatic: build features first, then add security incrementally.
But how do you actually do that without breaking everything?
The Incremental Migration Strategy
The key is to start permissive and gradually tighten security. Here’s the approach I’ve used successfully:
┌─────────────────────────────────────────────────────────────────┐│ Migration Phases │├─────────────────────────────────────────────────────────────────┤│ ││ Phase 1: Add dependency + permit all ││ ↓ ││ Phase 2: Secure admin/critical endpoints only ││ ↓ ││ Phase 3: Add authentication mechanism (JWT/Session) ││ ↓ ││ Phase 4: Secure remaining endpoints ││ ↓ ││ Phase 5: Enable CSRF and finalize ││ │└─────────────────────────────────────────────────────────────────┘Phase 1: Add Dependency Without Breaking Changes
First, add the dependency to your pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>Immediately after adding this, create a permissive configuration that allows everything:
@Configuration@EnableWebSecuritypublic class InitialSecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .anyRequest().permitAll() ) .csrf(csrf -> csrf.disable());
return http.build(); }
@Bean public UserDetailsService userDetailsService() { // Disable default generated user during migration return new InMemoryUserDetailsManager(); }}This configuration does something crucial: it makes Spring Security active but harmless. All your existing endpoints continue to work as before, but now you have the infrastructure in place to start securing things.
Run your tests. They should all pass. If they don’t, fix them now before moving forward.
Phase 2: Audit Your Endpoints
Before securing anything, you need to know what you have. I created a simple auditor that logs all endpoints on startup:
@Component@Slf4jpublic class EndpointAuditor implements ApplicationListener<ContextRefreshedEvent> {
@Autowired private RequestMappingHandlerMapping handlerMapping;
@Override public void onApplicationEvent(ContextRefreshedEvent event) { handlerMapping.getHandlerMethods().forEach((info, method) -> { String pattern = info.getPatternValues().toString(); String httpMethod = info.getMethodsCondition().getMethods().toString(); String controller = method.getBeanType().getSimpleName(); String methodName = method.getMethod().getName();
log.info("Endpoint: {} {} -> {}.{}", httpMethod, pattern, controller, methodName); }); }}This produces output like:
Endpoint: [GET] [/api/users] -> UserController.findAllEndpoint: [POST] [/api/users] -> UserController.createEndpoint: [GET] [/api/admin/stats] -> AdminController.getStatsEndpoint: [POST] [/api/auth/login] -> AuthController.loginNow categorize your endpoints:
┌─────────────────┬──────────────────────────────────────┐│ Category │ Examples │├─────────────────┼──────────────────────────────────────┤│ Public │ /api/auth/**, /api/public/** ││ User Protected │ /api/users/**, /api/posts/** ││ Admin Only │ /api/admin/**, /actuator/** │└─────────────────┴──────────────────────────────────────┘Phase 3: Secure Critical Endpoints First
Start with the most sensitive endpoints. Admin endpoints are usually the best candidates because they have the clearest access requirements:
@Configuration@EnableWebSecuritypublic class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().permitAll() // Everything else still open );
return http.build(); }}At this stage, your regular users won’t notice any difference. Only admin endpoints require authentication. This lets you:
- Test your authentication mechanism in isolation
- Verify role-based access control works
- Keep development moving on other features
Phase 4: Add Your Authentication Mechanism
Now add whatever authentication method fits your application. For a JWT-based API:
@Configuration@EnableWebSecurity@RequiredArgsConstructorpublic class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); }}For a traditional web application with sessions:
@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .permitAll() ) .logout(logout -> logout .logoutSuccessUrl("/login?logout") .permitAll() );
return http.build();}Handling Common Migration Issues
Swagger UI Stops Working
Add a separate security filter chain for documentation endpoints:
@Configurationpublic class SwaggerSecurityConfig {
@Bean @Order(1) public SecurityFilterChain swaggerFilterChain(HttpSecurity http) throws Exception { http.securityMatcher( "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**" ) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) .csrf(csrf -> csrf.disable());
return http.build(); }}CORS Errors from Frontend
Configure CORS properly during migration:
@Configurationpublic class CorsConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000", "http://localhost:4200") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); }}Tests Start Failing
Update your tests to use Spring Security’s testing support:
@SpringBootTest@AutoConfigureMockMvcclass UserControllerTest {
@Autowired private MockMvc mockMvc;
@Test @WithMockUser(roles = "USER") void shouldReturnUsersForAuthenticatedUser() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isOk()); }
@Test void shouldDenyAccessForUnauthenticatedUser() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isUnauthorized()); }}The Complete Migration Checklist
I’ve learned to follow this checklist every time:
- Add dependency with permissive configuration
- Run all tests and verify baseline
- Audit existing endpoints
- Categorize: public vs protected vs admin
- Secure admin endpoints first
- Add authentication mechanism
- Add role-based authorization
- Update tests for security context
- Fix Swagger/OpenAPI access
- Configure CORS for frontend
- Secure remaining endpoints
- Enable CSRF for web apps (not APIs)
- Remove permissive rules
Why This Approach Works
The incremental approach works because it isolates problems. When you add security gradually:
- Each change is testable - You know exactly what broke
- Development continues - Team members aren’t blocked
- Learning curve is manageable - You learn one concept at a time
- Rollback is easy - Just revert to the previous configuration
- Security is measurable - You can see exactly what’s protected
As one Reddit commenter put it: “Build some functionality and then wire up the security to your role’s needs. That way is so much easier to determine how basic or engineered your implementation needs to be.”
When to Add Spring Security
The original question was: should you add security at the beginning or after completing the project?
The answer is neither. Add security when you need it, but do it incrementally:
- Don’t add at the very beginning - You’ll over-engineer for threats that don’t exist yet
- Don’t wait until the end - You’ll have too much to secure at once
- Add when you have endpoints to protect - You’ll have clear requirements
Another commenter’s advice resonated: “I am adding spring security in a project when I need some auth mechanism or some endpoint/service that requires auth.”
This is the right approach. Security should match your application’s needs, not precede them.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Security Reference Documentation
- 👨💻 Spring Boot Security Auto-Configuration
- 👨💻 When to add Spring Security in Spring Boot project - Reddit Discussion
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments