Skip to content

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:

  1. All endpoints require authentication by default
  2. A generated password is created for the default user
  3. Your existing tests start failing
  4. Frontend integration breaks
  5. 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:

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:

InitialSecurityConfig.java
@Configuration
@EnableWebSecurity
public 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:

EndpointAuditor.java
@Component
@Slf4j
public class EndpointAuditor implements ApplicationListener&lt;ContextRefreshedEvent&gt; {
@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 audit output
Endpoint: [GET] [/api/users] -> UserController.findAll
Endpoint: [POST] [/api/users] -> UserController.create
Endpoint: [GET] [/api/admin/stats] -> AdminController.getStats
Endpoint: [POST] [/api/auth/login] -> AuthController.login

Now 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:

SecurityConfig.java
@Configuration
@EnableWebSecurity
public 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:

  1. Test your authentication mechanism in isolation
  2. Verify role-based access control works
  3. 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:

SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public 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:

SecurityConfig.java
@Bean
public 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:

SwaggerSecurityConfig.java
@Configuration
public 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:

CorsConfig.java
@Configuration
public 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:

UserControllerTest.java
@SpringBootTest
@AutoConfigureMockMvc
class 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:

  1. Each change is testable - You know exactly what broke
  2. Development continues - Team members aren’t blocked
  3. Learning curve is manageable - You learn one concept at a time
  4. Rollback is easy - Just revert to the previous configuration
  5. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments